cli_mirror
Version:
A tool and library for using then Google Glass Mirror API from node.js
1,014 lines (881 loc) • 26.7 kB
JavaScript
;
// richard.prinz@min.at 2014
// http://www.min.at/prinz/cli_mirror
// ============================================================================
// Variables
var Q = require('q'),
path = require('path'),
util = require('util'),
sprintf = require('sprintf').sprintf,
colors = require('colors'),
http = require('http'),
fs = require('fs'),
fsEx = require('node-fs'),
spawn = require('open'),
async = require('async'),
mime = require('mime'),
googleapis = require('googleapis'),
OAuth2Client = googleapis.OAuth2Client;
// ============================================================================
// Constructor
/**
* Constructs a Glass object for using the Google Glass Mirror API.
* @constructor
* @param {Object} args A configuration object containing the following
* fields (all optional):
* appConfig Object An object containing OAuth app tokens created
* by the Google Project Console
* https://console.developers.google.com/project
* glassConfig Object An object containing OAuth tokens for a
* specific Google Account
* glassID String An ID of Glass Device. This can be any name
* as long as it uniquely identifies a specific device.
* Defaults to "default"
* logMode Integer Defines how informations are written to
* the console. 0 - normal, 1 - debug, 2 - quiet
*
* If both "glassConfig" and "glassID" are specified, "glassID" is ignored.
*/
function Glass(args) {
var self = this;
self.LOG_DEBUG = 0;
self.LOG_INFO = 1;
self.LOG_OK = 2;
self.LOG_WARNING = 3;
self.LOG_ERROR = 4;
var APP_CONFIG_STORE = './config.json';
self.appConfig = (args ? args['appConfig'] : null);
// ensure cli_mirror has infos about a Google project
// see http://www.min.at/prinz/cli_mirror#project
// for more infos
self.appConfigStore = path.resolve(path.dirname(process.mainModule.filename),
APP_CONFIG_STORE);
self.glassConfig = (args ? args['glassConfig'] : null);
self.glassID = (args ? args['glassID'] : null);
self._prepareGlassConfigStore(self.glassID);
// 0 - normal, 1 - debug, 2 - quiet
self.logMode = (args ? args['logMode'] : 0);
if(!self._isInt(self.logMode))
self.logMode = 0;
self.logMode = Math.abs(self.logMode);
if(self.logMode < 0 || self.logMode > 2)
self.logMode = 0;
}
// ============================================================================
// Public Methods
/**
* Connects to Googles Glass Mirror API and provides a text mode user interface
* in case the given glassID was not already registered
*
* @param {String} glassID Optional id of Glass Device. This can be any name
* as long as it uniquely identifies a specific device. Defaults to
* "default"
* @param {function()} errorCallback Optional callback function in case of
* any errors
* @param {function()} successCallback Optional callback function called
* when connect was successful
*/
Glass.prototype.connectGui = function(glassID, errorCallback, successCallback) {
var self = this;
var msg;
// read app.config if no one was given during object creation
if(!self.appConfig) {
if(!fs.existsSync(self.appConfigStore)) {
msg = 'app.config not found at ' +
self.appConfigStore + '!\r\n' +
'See http://www.min.at/prinz/cli_mirror#project for more infos.';
if(errorCallback) {
errorCallback(msg);
return;
}
}
try {
self.appConfig = require(self.appConfigStore);
}
catch(error) {
msg = 'Unable to use existing app config at ' +
self.appConfigStore + '!\r\n' +
'See http://www.min.at/prinz/cli_mirror#project for more infos.'
if(errorCallback) {
errorCallback(msg);
return;
}
}
}
// create a glass id config file if no glass config file was given
// during object cration
if(!self.glassConfig) {
if(!self.glassID) {
self.glassID = glassID;
self._prepareGlassConfigStore(self.glassID);
}
try {
if(fs.existsSync(self.glassConfigStore)) {
self.logInfo('Existing Google OAuth tokens available for ' +
'Google Glass (' + self.glassID.magenta + ')');
self.glassConfig = require(self.glassConfigStore);
}
}
catch(error) {
self.logError('Unable to use existing Google OAuth tokens', error);
}
if (!self.glassConfig) {
self.logWarning('Google OAuth tokens missing or invalid for ' +
'Google Glass (' + self.glassID.magenta + ')');
}
}
self.connect(
// requestCodeCallback
function(url) {
self.logWarning('Requesting new Google OAuth tokens');
return self._requestCode(url, errorCallback, function(code) {
return code
});
},
// saveTokenCallback
function(tokens) {
fs.writeFileSync(self.glassConfigStore,
JSON.stringify(tokens, null, 4));
self.logOK('Google OAuth tokens saved for ' +
'Google Glass (' + self.glassID.magenta + ')');
self.logDebug('Tokens:', tokens);
},
// errorCallback
errorCallback,
// successCallback
successCallback);
}
/**
* Connects to Googles Glass Mirror API without providing any GUI. Instead
* all interactions are done by using callbacks.
*
* @param {function()} requestCodeCallback Optional callback function in case
* of a missing Glass config. Callback must call Google to retrieve
* an OAuth code for the given glassID and return this using a value or
* promise
* @param {function()} saveTokenCallback Optional callback function to save
* OAuth tokens for later use
* @param {function()} errorCallback Optional callback function in case of
* any errors
* @param {function()} successCallback Optional callback function called
* when connect was successful
*/
Glass.prototype.connect = function(requestCodeCallback, saveTokenCallback,
errorCallback, successCallback) {
var self = this;
// init OAuth client with Google Project credentials
self.oauth2Client = new OAuth2Client(
self.appConfig.installed.client_id,
self.appConfig.installed.client_secret,
self.appConfig.installed.redirect_uris[0]);
self.oauth2Client.credentials = self.glassConfig;
if (!self.oauth2Client.credentials) {
var url = self.oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: 'https://www.googleapis.com/auth/glass.timeline'
});
if(requestCodeCallback) {
Q(requestCodeCallback(url)).then(function(code) {
if(code) {
self.oauth2Client.getToken(code, function(error, tokens){
if(error) {
if(errorCallback)
errorCallback(error);
return;
}
else {
self.oauth2Client.credentials = tokens;
if(saveTokenCallback)
saveTokenCallback(tokens);
self._connect(errorCallback, successCallback);
}
});
}
else {
if(errorCallback)
errorCallback('no oAuth code provided');
return;
}
});
}
else {
if(errorCallback)
errorCallback('no requestCodeCallback specified');
return;
}
}
else {
self._connect(errorCallback, successCallback);
}
}
/**
* Logs text to the console using a [DBG] prefix in dark grey colour and
* optionally an objects properties
*
* @param {String} message Optional message to display on console
* @param {Object} object Optional object who's properties should be
* displayed under the message text
* @param {Boolean} logTime Optional Whether or not to display a time stamp
* in the log entry
*/
Glass.prototype.logDebug = function(message, object, logTime) {
var self = this;
if(typeof logTime === 'undefined')
logTime = true;
self.log(self.LOG_DEBUG, logTime, message, object);
}
/**
* Logs text to the console using a [INF] prefix in white and
* optionally an objects properties
*
* @param {String} message Optional message to display on console
* @param {Object} object Optional object who's properties should be
* displayed under the message text
* @param {Boolean} logTime Optional Whether or not to display a time stamp
* in the log entry
*/
Glass.prototype.logInfo = function(message, object, logTime) {
var self = this;
if(typeof logTime === 'undefined')
logTime = true;
self.log(self.LOG_INFO, logTime, message, object);
}
/**
* Logs text to the console using a [OK ] prefix in green colour and
* optionally an objects properties
*
* @param {String} message Optional message to display on console
* @param {Object} object Optional object who's properties should be
* displayed under the message text
* @param {Boolean} logTime Optional Whether or not to display a time stamp
* in the log entry
*/
Glass.prototype.logOK = function(message, object, logTime) {
var self = this;
if(typeof logTime === 'undefined')
logTime = true;
self.log(self.LOG_OK, logTime, message, object);
}
/**
* Logs text to the console using a [WRN] prefix in yellow and
* optionally an objects properties
*
* @param {String} message Optional message to display on console
* @param {Object} object Optional object who's properties should be
* displayed under the message text
* @param {Boolean} logTime Optional Whether or not to display a time stamp
* in the log entry
*/
Glass.prototype.logWarning = function(message, object, logTime) {
var self = this;
if(typeof logTime === 'undefined')
logTime = true;
self.log(self.LOG_WARNING, logTime, message, object);
}
/**
* Logs text to the console using a [ERR] prefix in red colour and
* optionally an objects properties
*
* @param {String} message Optional message to display on console
* @param {Object} object Optional object who's properties should be
* displayed under the message text
* @param {Boolean} logTime Optional Whether or not to display a time stamp
* in the log entry
*/
Glass.prototype.logError = function(message, object, logTime) {
var self = this;
if(typeof logTime === 'undefined')
logTime = true;
self.log(self.LOG_ERROR, logTime, message, object);
}
/**
* Logs text to the console using a selectable prefix and colour and
* optionally an objects properties
*
* @param {Integer} severity Selects how a message should be displayed.
* See also the LOG_* constants.
* LOG_DEBUG = 0
* LOG_INFO = 1
* LOG_OK = 2
* LOG_WARNING = 3
* LOG_ERROR = 4
* @param {Boolean} logTime Optional Whether or not to display a time stamp
* in the log entry
* @param {String} message Optional message to display on console
* @param {Object} object Optional object who's properties should be
* displayed under the message text
*/
Glass.prototype.log = function(severity, logTime, message, object) {
var self = this;
var ts = '';
var w;
// 0 - normal
// 1 - debug
// 2 - quiet
if(self.logMode == 2)
return;
if(logTime === true) {
var now = new Date();
ts = sprintf(' %04d%02d%02d %02d%02d.%-4d',
now.getFullYear(),
now.getMonth() + 1,
now.getDate(),
now.getHours(),
now.getMinutes(),
now.getSeconds(),
now.getMilliseconds());
}
switch(severity) {
case self.LOG_DEBUG:
if(self.logMode == 1) {
w = '[DBG' + ts + '] ';
console.log(w.grey + message);
}
break;
case self.LOG_OK:
w = '[OK ' + ts + '] ';
console.log(w.green + message);
break;
case self.LOG_WARNING:
w = '[WRN' + ts + '] ';
console.log(w.yellow + message);
break;
case self.LOG_ERROR:
w = '[ERR' + ts + '] ';
console.log(w.red + message);
break;
default:
w = '[INF' + ts + '] ';
console.log(w.white + message);
break;
}
if(object)
if(severity > 0 || self.logMode == 1)
console.log(object);
}
/**
* Retrieves ALL timeline entries from the connected Glass and returns a
* Mirror API timeline object
*
* @param {function()} errorCallback Optional callback function in case of
* any errors
* @param {function()} successCallback Optional callback function called
* when API call was successful
*/
Glass.prototype.listTimeline = function(errorCallback, successCallback) {
var self = this;
self._listTimeline(self.mirrorApi, errorCallback, successCallback);
}
/**
* Retrieves ALL timeline entries from the connected Glass and returns an
* array of timeline entry ID's
*
* @param {function()} errorCallback Optional callback function in case of
* any errors
* @param {function()} successCallback Optional callback function called
* when API call was successful
*/
Glass.prototype.listTimelineIDs = function(errorCallback, successCallback) {
var self = this;
self._listTimeline(self.mirrorApi, errorCallback, function(data) {
var ids = [];
data.items.map(function(item) {
ids.push(item.id);
});
if(successCallback)
successCallback(ids);
});
}
Glass.prototype.createTimelineEntry = function(content,
attachment, lat, lon, marker, zoom, isJSON,
errorCallback, successCallback) {
var self = this;
var attachmentMime;
if(!attachment || attachment == '-' || !fs.existsSync(attachment)) {
if(attachment && attachment != '-')
self.logWarning('Attachment specified (' + attachment +
') but not found.');
else {
if(lat && lon) {
if(!/^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)$/.test(lat)) {
if(errorCallback)
errorCallback('Invalid latitude ('+ lat +')');
return;
}
if(!/^[-+]?((1[0-7]\d)|([1-9]?\d))(\.\d+)?|(180(\.0+)?)$/.test(lon)) {
if(errorCallback)
errorCallback('Invalid longitude ('+ lon +')');
return;
}
if(!/^[0-9]|1[0-9]|-$/.test(zoom))
zoom = '-';
self.getMap(lat, lon, zoom, marker,
errorCallback,
function(mapMime, mapImage) {
self._timelineInsert(self.mirrorApi, content,
mapMime, mapImage,
errorCallback, successCallback,
isJSON);
});
return;
}
}
attachment = null
attachmentMime = null;
}
else {
attachmentMime = mime.lookup(attachment);
attachment = fs.readFileSync(attachment);
}
self._timelineInsert(self.mirrorApi, content,
attachmentMime, attachment,
errorCallback, successCallback,
isJSON);
}
/**
* Retrieves one timeline entrie by its unique id and returns a Mirror API
* timeline entry object or null if not found
*
* @param {String} entryID The unique ID of the timelie entry to read
* @param {function()} errorCallback Optional callback function in case of
* any errors
* @param {function()} successCallback Optional callback function called
* when API call was successful
*/
Glass.prototype.readTimelineEntry = function(entryID,
errorCallback, successCallback) {
var self = this;
self._timelineGet(self.mirrorApi, entryID, errorCallback, successCallback);
}
Glass.prototype.updateTimelineEntry = function(entryID, content,
attachment, lat, lon, marker, zoom, isJSON,
errorCallback, successCallback) {
var self = this;
var attachmentMime;
if(!attachment || attachment == '-' || !fs.existsSync(attachment)) {
if(!attachment && attachment != '-')
self.logWarning('Attachment specified (' + attachment +
') but not found.');
else {
if(lat && lon) {
if(!/^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)$/.test(lat)) {
if(errorCallback)
errorCallback('Invalid latitude ('+ lat +')');
return;
}
if(!/^[-+]?((1[0-7]\d)|([1-9]?\d))(\.\d+)?|(180(\.0+)?)$/.test(lon)) {
if(errorCallback)
errorCallback('Invalid longitude ('+ lon +')');
return;
}
if(!/^[0-9]|1[0-9]|-$/.test(zoom))
zoom = '-';
self.getMap(lat, lon, zoom, marker,
errorCallback,
function(mapMime, mapImage) {
self._timelineUpdate(self.mirrorApi, entryID, content,
mapMime, mapImage,
errorCallback, successCallback,
isJSON);
});
return;
}
}
attachment = null
attachmentMime = null;
}
else {
attachmentMime = mime.lookup(attachment);
attachment = fs.readFileSync(attachment);
}
self._timelineUpdate(self.mirrorApi, entryID, content,
attachmentMime, attachment,
errorCallback, successCallback, isJSON);
}
/**
* Deletes one timeline entrie by its unique id and returns a Mirror API
* timeline entry object or null if not deleted
*
* @param {String} entryID The unique ID of the timelie entry to delete
* @param {function()} errorCallback Optional callback function in case of
* any errors
* @param {function()} successCallback Optional callback function called
* when API call was successful
*/
Glass.prototype.deleteTimelineEntry = function(entryID,
errorCallback, successCallback) {
var self = this;
self._timelineDelete(self.mirrorApi, entryID, errorCallback, successCallback);
}
/**
* Deletes ALL timeline entries from the timeline and returns the number
* of deleted entries
*
* @param {function()} errorCallback Optional callback function in case of
* any errors
* @param {function()} successCallback Optional callback function called
* when API call was successful
*/
Glass.prototype.deleteTimeline = function(errorCallback, successCallback) {
var self = this;
var counter = 0;
self._listTimeline(self.mirrorApi, errorCallback, function(data) {
var deleteJobs = [];
data.items.map(function(item) {
deleteJobs.push(function(callback) {
self._timelineDelete(self.mirrorApi, item.id,
function(error) {
callback(error, null)
},
function(data) {
counter++
callback(null, data);
});
});
});
async.series(deleteJobs, function(errorCallback, data) {
if(successCallback)
successCallback(counter);
});
});
}
Glass.prototype.getMapUrl = function(lat, lon, zoom, marker) {
var position;
if(marker && marker != '-')
position = 'markers=color:red%7Clabel:' + marker.toString()[0] +
'%7C' + lat + ',' + lon;
else {
marker = null;
position = 'center=' + lat + ',' + lon;
}
if(zoom == '-')
zoom = null;
if(!marker && !zoom)
zoom = 12;
if(zoom)
zoom = 'zoom=' + zoom;
var mapUrl = 'http://maps.googleapis.com/maps/api/staticmap?' +
position + '&' +
zoom +'&' +
'size=640x320&format=png&sensor=false';
return mapUrl;
}
Glass.prototype.getMap = function(lat, lon, zoom, marker,
errorCallback, successCallback) {
var self = this;
var mapUrl = self.getMapUrl(lat, lon, zoom, marker);
http.get(mapUrl, function(res) {
var buffers = [];
var length = 0;
res.on('error', function(error) {
if(errorCallback)
errorCallback(error);
return;
});
res.on('data', function(chunk) {
length += chunk.length;
buffers.push(chunk);
});
res.on('end', function() {
var mapImage = Buffer.concat(buffers);
// determine the type of the image
// with image/jpeg being the default
var mapMime = 'image/jpeg';
if(res.headers['content-type'] !== undefined)
mapMime = res.headers['content-type'];
if(successCallback)
successCallback(mapMime, mapImage);
});
});
}
// ============================================================================
// Private Methods
// ----------------------------------------------------------------------------
// Google Glass Mirror API
Glass.prototype._timelineInsert = function(client,
message, attachmentMime, attachment,
errorCallback, successCallback, isJSON) {
// insert message into timeline
var self = this;
var msg;
if(message[0] == '@') {
var messageFile = message.substring(1);
if(!fs.existsSync(messageFile)) {
var error = 'Timeline entry contentfile (' + messageFile + ') not found';
if(errorCallback)
errorCallback(error);
return;
}
message = fs.readFileSync(messageFile);
}
if(isJSON)
msg = JSON.parse(message);
else
msg = {
'text': message,
'menuItems': [{'action': 'DELETE'}]
}
client
.mirror.timeline.insert(msg)
.withAuthClient(self.oauth2Client)
.execute(function(error, data, res) {
if (error) {
if(errorCallback)
errorCallback(error);
return;
}
else {
var timelineEntry = data;
// if attachment available - insert it
if(attachmentMime && attachment) {
self.logDebug('Add attachment to timeline id: ', timelineEntry.id);
client
.mirror.timeline.attachments.insert({
'itemId': timelineEntry.id,
'uploadType': 'media'
})
.withMedia(attachmentMime, attachment)
.withAuthClient(self.oauth2Client)
.execute(function(error, data, res) {
if (error) {
if(errorCallback)
errorCallback(error);
return;
}
else {
if(successCallback)
successCallback(timelineEntry);
}
});
}
else
if(successCallback)
successCallback(timelineEntry);
}
});
};
Glass.prototype._timelineUpdate = function(client,
entryID, message, attachmentMime, attachment,
errorCallback, successCallback, isJSON) {
// update entry in the timeline
var self = this;
var msg;
if(message[0] == '@') {
var messageFile = message.substring(1);
if(!fs.existsSync(messageFile)) {
var error = 'Timeline entry contentfile (' + messageFile + ') not found';
if(errorCallback)
errorCallback(error);
else
doExit(error);
}
message = fs.readFileSync(messageFile);
}
if(isJSON)
msg = JSON.parse(message);
else
msg = {
'text': message,
'menuItems': [{'action': 'DELETE'}]
}
client
.mirror.timeline.patch({'id': entryID}, msg)
.withAuthClient(self.oauth2Client)
.execute(function(error, data, res) {
if (error) {
if(errorCallback)
errorCallback(error);
}
else {
var timelineEntry = data;
// if attachment available - insert it
if(attachmentMime && attachment) {
self.logDebug('Add attachment to timeline id: ', timelineEntry.id);
client
.mirror.timeline.attachments.insert({
'itemId': timelineEntry.id,
'uploadType': 'media'
})
.withMedia(attachmentMime, attachment)
.withAuthClient(self.oauth2Client)
.execute(function(error, data, res) {
if (error) {
if(errorCallback)
errorCallback(error);
}
else {
if(successCallback)
successCallback(timelineEntry);
}
});
}
else
if(successCallback)
successCallback(timelineEntry);
}
});
};
Glass.prototype._timelineDelete = function(client,
entryID,
errorCallback, successCallback) {
// delete timeline entry
var self = this;
client
.mirror.timeline.delete({
'id': entryID
})
.withAuthClient(self.oauth2Client)
.execute(function(error, data, res) {
if (error) {
if(errorCallback)
errorCallback(error);
}
else {
if(successCallback)
successCallback(data);
}
});
}
Glass.prototype._timelineGet = function(client,
entryID,
errorCallback, successCallback) {
// get timeline entry by id
var self = this;
client
.mirror.timeline.get({
'id': entryID
})
.withAuthClient(self.oauth2Client)
.execute(function(error, data, res) {
if (error) {
if(errorCallback)
errorCallback(error);
}
else {
if(successCallback)
successCallback(data);
}
});
}
Glass.prototype._insertContact = function(client,
errorCallback, successCallback) {
var self = this;
client
.mirror.contacts.insert({
"id": "prinz",
"displayName": "Richard Prinz",
"iconUrl": "http://www.min.at/prinz/rprinz.png",
"priority": 7,
"acceptCommands": [
{"type": "POST_AN_UPDATE"},
{"type": "TAKE_A_NOTE"}
]
})
.withAuthClient(self.oauth2Client)
.execute(function(error, data) {
if (error) {
if(errorCallBack)
errorCallback(error);
}
else {
if(successCallback)
successCallback(data);
}
});
};
Glass.prototype._listTimeline = function(client,
errorCallback, successCallback) {
var self = this;
client
.mirror.timeline.list()
.withAuthClient(self.oauth2Client)
.execute(function(error, data, res) {
if (error) {
if(errorCallback)
errorCallback(error);
}
else {
if(successCallback)
successCallback(data);
}
});
};
// ----------------------------------------------------------------------------
// Misc Methods
Glass.prototype._connect = function(errorCallback, successCallback) {
var self = this;
googleapis
.discover('mirror', 'v1')
.execute(function(error, client) {
if(error) {
if(errorCallback)
errorCallback(error);
return
}
self.mirrorApi = client;
if(successCallback)
successCallback();
});
}
Glass.prototype._prepareGlassConfigStore = function() {
var self = this;
var OAUTH_STORE = './glasses/';
var DEFAULT_GLASS_ID = 'default';
self.glassID = (!self.glassID ? DEFAULT_GLASS_ID : self.glassID);
self.glassID = self.glassID.replace(/[^a-zA-Z0-9_-]/g, '');
self.glassConfigStore = path.resolve(path.dirname(process.mainModule.filename), OAUTH_STORE);
fsEx.mkdirSync(self.glassConfigStore, 755, true);
self.glassConfigStore = path.join(self.glassConfigStore, self.glassID + '.json');
}
Glass.prototype._requestCode = function(requestUrl,
errorCallback, successCallback) {
var self = this;
var d = Q.defer();
// call google oauth service. url according to:
// https://developers.google.com/accounts/docs/OAuth2WebServer
console.log('\r\n\r\Use your browser to open this URL:\r\n');
console.log(requestUrl.cyan);
console.log('\r\nThen come back and enter the CODE here\r\n');
// start user browser for oauth registration
spawn(requestUrl, function(error) {
if(error) {
log(LOG_ERROR, 'Error starting user web browser', error);
if(errorCallback)
errorCallback(error);
return;
}
});
// ask user for code parameter
self._ask('CODE', /.+/, function(code) {
console.log();
d.resolve(code);
if(successCallback)
successCallback(code);
});
return d.promise;
}
Glass.prototype._ask = function(question, format, callback) {
var self = this;
var stdin = process.stdin
var stdout = process.stdout;
stdin.resume();
stdout.write(question + ": ");
stdin.once('data', function(data) {
data = data.toString().trim();
if(format.test(data)) {
if(callback)
callback(data);
}
else {
stdout.write("It should match: "+ format +"\n");
self._ask(question, format, callback);
}
});
}
Glass.prototype._isInt = function(i_int) {
var i = parseInt(i_int);
if (isNaN(i))
return false;
return i_int == i && i_int.toString() == i.toString();
}
module.exports.Glass = Glass;