Source: cloud.js

/**
 * @overview  defines methods that can be used by a web service for tracking vehicles 
 */

/* jshint -W097, -W083 */
"use strict";

var KaisekiInc = require('kaiseki'),
    xmpp = require('node-xmpp'),
    // underscorejs
    // collection of useful Javascript functions
    // see http://underscorejs.org/
    _ = require('underscore'),
    util = require('./util.js'),
    config = require('./config.js'),
    timestamp = util.timestamp,
    diffTime = util.diffTime,

    // credentials for Jabber
    jabber_creds = {
        username: 'ridekeeper',
        password: 'cs117ridekeeper',
        jid: '718168-5815@chat.quickblox.com',
        room_jid: '5815_%s@muc.chat.quickblox.com',
        host: 'chat.quickblox.com',
        port: 5222,
        nickname: 'Big_Brother'
    },

    // Kaiseki Instances
    kaiseki = new KaisekiInc(config.APP_ID, config.REST_API_KEY),

    // the time difference between stolenDate and recoveredDate
    // this threshold determines when to reset vechiles to a clean not stolen state
    // 5 days in milliseconds
    REFRESH_THRESHOLD = 5 * 24 * 60 * 60 * 1000,

    // Alert Constants
    AlertLevel = {
        PARKED: 0, // do nothing
        MOVED:  1, // Slight movement (eg:tilt) no broken geofence yet notify user only 
        MOVED_LOC: 2,
        STOLEN: 3, // Broken geofence alert nearby users
        STOLEN_LOC: 4,
        RIDING: 5,
        CRASHED: 6,

        // TODO: change this to numbers?
        NRD:    "NRD", // not recovered
        RVD:    "RVD" // recovered
    },
    // dictionary to look up alert level name by integer value
    reverseAlertLevelDictionary = _.invert(AlertLevel);

/**
 * @function sendPushNotification
 *
 * @desc sends push notification to device based off of vehicle's owner id
 *
 * @param {Number} alertKey - the number corresponding the type of alert that is sent
 * @param {Object} vehicle - the vehicle and its metadata
 * @param {String} message - the message that we are sending
 */
function sendPushNotification(alertKey, vehicle, message) {
    var notification_data = {   // Data for Nofication
        where: { ownerId: vehicle.ownerId },
        data: {
            action: 'CUSTOMIZED',
            alertLevel: alertKey,
            message: message
        }
    };

    // Notify users via Push
    kaiseki.sendPushNotification(notification_data, function(err, res, body, success) {
        console.log(notification_data);

        if (success) {

            console.log('Owner notified.');

        } else {

            console.log(body.error);
        }
    });
}

/**
 * @function updateVehicleStatus
 *
 * @desc Update the vehicle's alert level and location
 * and send notification to owner
 *
 * @param {Object}  alert  - the metadata associated with the vehicle tracker
 * @param {Object}  vehicle - the vehicle and its metadata
 * @param {String}  message  - the message that we are sending 
 */
function updateVehicleStatus(alert, vehicle, message) {

    var alertKey = reverseAlertLevelDictionary[(parseInt(alert.lvl)).toString()],
        data = {
            alertLevel: alertKey,
            pos: alert.location
        };

    // store a timestamp of date vehicle is stolen
    if (alertKey === AlertLevel.STOLEN) {
        data.stolenDate = timestamp();
        data.recoveredDate = undefined;
    }

    // store a timestamp of date when vehicle is recovered
    if (alertKey === AlertLevel.RVD) {
        data.recoveredDate = timestamp();
    }

    kaiseki.updateObject('Vehicle', vehicle.objectId, data,
    function(err, res, body, success) {
        //send notifications w.r.t. the alert level
        if (err) {
            console.log(body.error);
        } else {
            if(message != null) {
                sendPushNotification(alertKey, vehicle, message);
            }
        }
    });
}

/**
 * @function updateVehicleLocation
 *
 * @param {Object}  alert  - the metadata associated with the vehicle tracker
 * @param {Object}  vehicle - the vehicle and its metadata
 */
function updateVehicleLocation(alert, vehicle) {

    kaiseki.createObject( 'VehicleLocationHistory', {
        'trackerId': alert.id,
        'ownerId': vehicle.ownerId,
        'pos': alert.location,
        'timestamp': timestamp()
    }, function(err, res, body, success) {

        if (!success) {
            console.log(body.error);
        }

    });
}


/**
 * @function notifyNearbyUsers
 *
 * @desc Cycle through every stolen vehicle and notify users within specified radius  that the vehicle is stolen.
 * 
 */
var notifyNearbyUsers = function() {
    console.log('Notifying users near stolen vehicles.');
    // First get all Vechiles that are stolen
    kaiseki.getObjects('Vehicle', {where: {alertLevel: "STOLEN"}}, function(err, res, body, success) {
        if (success) {
            console.log('we got some data back');   
            for (var i = 0; i < body.length; ++i) {
                var veh = body[i];
                console.log('Notifying users near vehicleId: ' + veh['objectId']);

                // refer to https://parse.com/docs/rest#geo
                var geopoint_where = {
                    GeoPoint: {
                        '$nearSphere': {
                            __type: 'GeoPoint',
                            'latitude': veh['pos']['latitude'],
                            'longitude': veh['pos']['longitude']
                        },
                        '$maxDistanceInMiles': 0.5
                    }
                };

                console.log("We get this far buddy");
                kaiseki.getUsers(geopoint_where, function(err, res, body, success) {

                    if (success) {
                        var nearby_users = [];

                        for (var j = 0; j < body.length; ++j) {
                            console.log(body[j]['objectId']);
                            nearby_users.push(body[j]['objectId']);
                        }

                        // get chatroom assocated with vehicle
                        kaiseki.getObjects('Chatroom', { where: { vehicleId : veh['objectId']},limit:1, order:'-createdAt' }, function(err, res, body, success) {

                            if (success) {
                                console.log('Number of chatrooms found: '+body.length);
                                for (var k = 0; k < body.length; ++k) {
                                    var room = body[k];

                                    if (!room['members']) {
                                        room['members'] = [];
                                    }

                                    var new_users = _.difference(nearby_users, room['members']),
                                        // add new users to chat room
                                        members = room['members'].concat(new_users);


                                    if (new_users.length > 0) {
                                        console.log("Added " + new_users.length + " new members: " + new_users);
                                    }

                                    // Update chatroom members with new users
                                    kaiseki.updateObject('Chatroom', room['objectId'], { members: members }, function(err, res, body, success) {
                                        if (success) {
                                            console.log('New members successfully added to chatroom.');
                                            // Send push notification to all new users regarding stolen vehicle 
                                            for (var j = 0; j < new_users.length; ++j) {
                                                var notification_data = {
                                                    where: { objectId: new_users[j] },
                                                    data: {
                                                        action: 'CUSTOMIZED',
                                                        alertLevel: 'NEARBY',
                                                        message: 'Time to roll out. Help us recover this ' + body.make + ' ' + body.model+".",
                                                        room: room['objectId']
                                                    }
                                                };
                                                kaiseki.sendPushNotification(notification_data, function(err, res, body, success) {
                                                    if (success) {
                                                        // don't want to flood the console...
                                                        // console.log('Push notification successfully sent:', body);
                                                    }
                                                    else {
                                                        console.log(body.error);
                                                    }
                                                });
                                            }
                                        }
                                        else {
                                            console.log(body.error);
                                        }
                                    });
                                }
                            } else {
                                console.log(body.error);
                            }
                        });
                    } else {
                        console.log(body.error);
                    }
                });
            }
        }
        else {
            console.log(body.error);
        }
    });
};


/**
 * createChatroom
 *
 * @desc Creates a chatroom for the stolen vehicle.
 *
 * @param {String} vehicleId - a vehicle's id
 */
function createChatroom(vehicleId) {

    var roomName = vehicleId + (new Date().getTime()).toString();

    // create chatroom
    kaiseki.createObject('Chatroom', { vehicleId: vehicleId, roomName: roomName }, function(err, res, body, success) {

        if (success) {

            var cl = new xmpp.Client({
                jid: jabber_creds.jid,
                password: jabber_creds.password,
                host: jabber_creds.host,
                port: jabber_creds.port
            });


            cl.on('online', function() {

                var room_jid = jabber_creds.room_jid.replace("%s", roomName);
                // join room (and request no chat history)
                cl.send(new xmpp.Element('presence', { to: room_jid + '/' + jabber_creds.nickname }).
                    c('x', { xmlns: 'http://jabber.org/protocol/muc' })
                );

                    // Request configuration form
                cl.send(new xmpp.Element('iq', { to: room_jid, id: 'create', type: 'get' }).
                    c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' })
                );

                // Submit configuration form
                cl.send(new xmpp.Element('iq', { to: room_jid, id: 'create', type: 'set' }).
                    c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' }).
                    c('x', { xmlns: 'jabber:x:data',type: 'submit' }).
                    // Set form type
                    c('field', { var: 'FORM_TYPE'}).
                    c('value').
                    t('http://jabber.org/protocol/muc#config').
                    up().up().
                    // Set persistent
                    c('field', { var: 'muc#roomconfig_persistentroom'}).
                    c('value').
                    t('1').
                    up().up().
                    // Set members only
                    c('field', { var: 'muc#roomconfig_membersonly'}).
                    c('value').
                    t('0').
                    up().up().
                    // Enable logging
                    c('field', { var: 'muc#roomconfig_enablelogging'}).
                    c('value').
                    t('1').
                    up().up().
                    // Set room name - room_id
                    c('field', { var: 'muc#roomconfig_roomname'}).
                    c('value').
                    t(vehicleId).
                    up().up().
                    // Set max history
                    c('field', { var: 'muc#maxhistoryfetch'}).
                    c('value').
                    t('1000')
                );

                // send keepalive data or server will disconnect us after 150s of inactivity
                setInterval(function() {
                    cl.send(new xmpp.Message({}));
                }, 30000);
            });
        } else {
            console.log(body.error);
        }
    });
}

/**
 * @function processTrackerAlert
 *
 * @desc process alert that is sent from vehicle tracker
 *
 * @param {Object} req - the request object
 * @param {Object} resp - the response object
 * @param {Function} next
 */
var processTrackerAlert = function(req, resp, next) {
    console.log('Got Data %s \n',req.body.id);

    // Parse message contents
    var alert = req.body;
    console.log(alert);

    // Find vehicle by trackerId
    kaiseki.getObjects(
        'Vehicle', {
            where: { trackerId: alert.id }, 
            limit: 1 
        }, function(err, res, body, success) {

            var vehicle = body[0],
                message = "";

            alert.location = {
                __type: 'GeoPoint',
                latitude: parseFloat(alert.lat),
                longitude: parseFloat(alert.lng)
            };

            // Respond to alert level accordingly
            switch (parseInt(alert.lvl)){
                case AlertLevel.PARKED:
                    message = vehicle.make + ' ' + vehicle.model + ' has been parked!';
                    updateVehicleStatus(alert, vehicle, message);
                    break;

                case AlertLevel.MOVED:
                    message = vehicle.make + ' ' + vehicle.model + ' has been moved!';
                    updateVehicleStatus(alert, vehicle, message);
                    break;

                case AlertLevel.MOVED_LOC:
                    updateVehicleLocation(alert, vehicle, null);
                    break;

                case AlertLevel.STOLEN:
                    message = vehicle.make + ' ' + vehicle.model + ' has been stolen!';
                    updateVehicleStatus(alert, vehicle, message);
                    createChatroom(vehicle.objectId);
                    break;

                case AlertLevel.STOLEN_LOC:
                    updateVehicleLocation(alert, vehicle, null);
                    break;

                case AlertLevel.RIDING:
                    updateVehicleLocation(alert, vehicle, null);
                    break;

                case AlertLevel.CRASHED:
                    message = vehicle.make + ' ' + vehicle.model + ' has been crashed!';
                    updateVehicleStatus(alert, vehicle, message);
                    break;
            }
        });
};

/**
 * @function refereshRecoveredVehicles
 *
 * @desc after a certain interval of vehicle recovery,
 *       resets vehicle status metadata
 *
 * @param {Function} onSuccess - callback on success
 * @param {Function} onError - callback on error
 */
var refreshRecoveredVehicles = function(onSuccess, onError) {

    kaiseki.getObjects(
        'Vehicle', {

            where: {
                recoveredDate: {$exists: true} 
            }

        }, function(err, res, body, success) {

            var diff = 0,
                deleteField = {__op: "Delete"};

            body.forEach(function(vehicle, index) {

                diff = diffTime(vehicle.recoveredDate, vehicle.stolenDate);

                if (diff !== false && diff >= REFRESH_THRESHOLD) {

                    kaiseki.updateObject('Vehicle', vehicle.objectId,
                    {
                        alertLevel: deleteField,
                        stolenDate: deleteField,
                        recoveredDate: deleteField,
                        status: deleteField 
                    },
                    function(err, res, body, success) {

                        if (success) {
                            if (onSuccess) {
                                onSuccess(body);
                            }
                        }

                        if (err) {
                            console.log(body.error);
                            if (onError) {
                                onError(body);
                            }
                        }
                    });
                }
            });
        });
};


module.exports = {
    processTrackerAlert:  processTrackerAlert,
    notifyNearbyUsers: notifyNearbyUsers,
    refreshRecoveredVehicles: refreshRecoveredVehicles
};