diff --git a/Facebook-ArrowDB-Connector/.gitignore b/Facebook-ArrowDB-Connector/.gitignore new file mode 100644 index 0000000..41f1f37 --- /dev/null +++ b/Facebook-ArrowDB-Connector/.gitignore @@ -0,0 +1,2 @@ +facebook.* +node_modules diff --git a/Facebook-ArrowDB-Connector/.npmignore b/Facebook-ArrowDB-Connector/.npmignore new file mode 100644 index 0000000..e819331 --- /dev/null +++ b/Facebook-ArrowDB-Connector/.npmignore @@ -0,0 +1,2 @@ +node_modules +logs diff --git a/Facebook-ArrowDB-Connector/README.md b/Facebook-ArrowDB-Connector/README.md new file mode 100644 index 0000000..4199ce5 --- /dev/null +++ b/Facebook-ArrowDB-Connector/README.md @@ -0,0 +1 @@ +# appc.facebook diff --git a/Facebook-ArrowDB-Connector/app.js b/Facebook-ArrowDB-Connector/app.js new file mode 100644 index 0000000..202ba44 --- /dev/null +++ b/Facebook-ArrowDB-Connector/app.js @@ -0,0 +1,17 @@ +/** + * NOTE: This file is simply for testing this connector and will not + * be used or packaged with the actual connector when published. + */ +var Arrow = require('arrow'), + server = new Arrow(); + +// TODO: Define a model that you can use when you run the connector locally for testing. +server.addModel(Arrow.Model.extend('tbray', { + fields: { + // TODO: Add fields to it. + title: {type: String} + }, + connector: 'appc.facebook' +})); + +server.start(); diff --git a/Facebook-ArrowDB-Connector/appc.json b/Facebook-ArrowDB-Connector/appc.json new file mode 100644 index 0000000..ab77844 --- /dev/null +++ b/Facebook-ArrowDB-Connector/appc.json @@ -0,0 +1,6 @@ +{ + "type":"connector", + "group":"arrow", + "dependencies": { + } +} \ No newline at end of file diff --git a/Facebook-ArrowDB-Connector/conf/.gitignore b/Facebook-ArrowDB-Connector/conf/.gitignore new file mode 100644 index 0000000..3519691 --- /dev/null +++ b/Facebook-ArrowDB-Connector/conf/.gitignore @@ -0,0 +1,3 @@ +local.js +*.local.js +deploy.json diff --git a/Facebook-ArrowDB-Connector/conf/.npmignore b/Facebook-ArrowDB-Connector/conf/.npmignore new file mode 100644 index 0000000..9d96847 --- /dev/null +++ b/Facebook-ArrowDB-Connector/conf/.npmignore @@ -0,0 +1,3 @@ +local.js +*.local.js +deploy.json \ No newline at end of file diff --git a/Facebook-ArrowDB-Connector/conf/default.js b/Facebook-ArrowDB-Connector/conf/default.js new file mode 100644 index 0000000..933077e --- /dev/null +++ b/Facebook-ArrowDB-Connector/conf/default.js @@ -0,0 +1,90 @@ +/** + * this is your configuration file defaults. + * + * You can create additional configuration files to that the server will load based on your + * environment. For example, if you want to have specific settings for production which are different + * than your local development environment, you can create a production.js and a local.js. Any changes + * in those files will overwrite changes to this file (a object merge is performed). By default, your + * local.js file will not be commited to git or the registry. + * + * This is a JavaScript file (instead of JSON) so you can also perform logic in this file if needed. + */ +module.exports = { + // these are your generated API keys. They were generated uniquely when you created this project. + // DO NOT SHARE these keys with other projects and be careful with these keys since they control + // access to your API using the default configuration. if you don't want two different keys for + // production and test (not recommended), use the key 'apikey'. To simulate running in production, + // set the environment variable NODE_ENV to production before running such as: + // + // NODE_ENV=production appc run + // + // production key, this is the key that will be required when you are running in production + apikey_production: 'xhA+NICf8qJy9r1+zKylI3ZiEegjl25J', + // development key, this is the key that will be required when you are testing non-production (such as locally) + apikey_development: 'a5EJlozJdCVIrFkgB87dOyzAW2huRN+G', + // preproduction key, this is the key that will be required when you are testing non-production (such as locally) + apikey_preproduction: 'FG+50Zn5qdNGyx76nUC3oHO0BCn8YtXo', + + // by default the authentication strategy is 'basic' which will use HTTP Basic Authorization where the + // usename is the key and the password is blank. the other option is 'apikey' where the value of the + // APIKey header is the value of the key. you can also set this to 'plugin' and define the key 'APIKeyAuthPlugin' + // which points to a file or a module that implements the authentication strategy + APIKeyAuthType: 'basic', + + // The number of milliseconds before timing out a request to the server. + timeout: 120000, + + // logging configuration + logLevel: 'debug', // Log level of the main logger. + logging: { + // location of the logs if enabled + logdir: './logs', + // turn on transaction logs + transactionLogEnabled: true + }, + + // prefix to use for apis + apiPrefix: '/api', + + // control the settings for the admin website + admin: { + // control whether the admin website is available + enabled: true, + // the prefix to the admin website + prefix: '/arrow', + // the prefix for the public apidocs website + apiDocPrefix: '/apidoc', + // if you set disableAuth, in production only your API docs will show up + disableAuth: false, + // if you set disableAPIDoc, your API docs will not show up (regardless of disableAuth) + disableAPIDoc: false, + // if you set disableDefault404, Arrow will not register a default 404 handler + disableDefault404: false, + // set to true to allow the admin website to be accessed in production. however, you will still need a + // login unless disableAuth is false. if you set this to false, the admin website will not be enabled + // when in production (still respects enabled above) + enableAdminInProduction: true, + // set the email addresses you want to be able to log in to the admin website + validEmails: ["nmasse@axway.com"], + // set the organization ids you want to be able to log in to the admin website + validOrgs: [100094705] + }, + + // you can generally leave this as-is since it is generated for each new project you created. + session: { + encryptionAlgorithm: 'aes256', + encryptionKey: '+aeBAIMvknm4yVebHQDZh9k2e6szb0s9DNIcWAetoEk=', + signatureAlgorithm: 'sha512-drop256', + signatureKey: 'OXTlgTMf+FT7TiByTtHtz9VQ/7CgPIu3Cxw4Ld/vWDCLWhwwxAZbPWH0Ytuwjs/cY6Ot8kEkQjPKMtnufKaQVg==', + secret: 'kjOmippGFVDL38Vyifc9Vow7Oq/wymcq', // should be a large unguessable string + duration: 86400000, // how long the session will stay valid in ms + activeDuration: 300000 // if expiresIn < activeDuration, the session will be extended by activeDuration milliseconds + }, + + // if you want signed cookies, you can set this value. if you don't want signed cookies, remove or make null + cookieSecret: 'zVciXXTRsEixUGUuVLteOOlhWlH2ppBx', + + // your connector configuration goes here + connectors: { + } +}; diff --git a/Facebook-ArrowDB-Connector/conf/example.config.js b/Facebook-ArrowDB-Connector/conf/example.config.js new file mode 100644 index 0000000..93babc5 --- /dev/null +++ b/Facebook-ArrowDB-Connector/conf/example.config.js @@ -0,0 +1,7 @@ +module.exports = { + connectors: { + 'appc.facebook': { + 'accessToken': '' + } + } +}; diff --git a/Facebook-ArrowDB-Connector/get-oauth-token.sh b/Facebook-ArrowDB-Connector/get-oauth-token.sh new file mode 100755 index 0000000..9e761b1 --- /dev/null +++ b/Facebook-ArrowDB-Connector/get-oauth-token.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +redirect_uri="http://localhost:8888/" + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +open "https://www.facebook.com/v2.8/dialog/oauth?client_id=$1&redirect_uri=$redirect_uri&scope=user_likes,manage_pages,user_posts" + +nc -l 8888 >facebook.authcode <

OK

+EOF + +code=$(sed -E -e 's|GET /[?]code=([^ ]+) HTTP/1[.]1|\1|' -e t -e d facebook.authcode |tr -d "\r\n") + +curl "https://graph.facebook.com/v2.8/oauth/access_token?client_id=$1&redirect_uri=$redirect_uri&client_secret=$2&code=$code" -o facebook.access_token + +jq -r ".access_token" facebook.access_token diff --git a/Facebook-ArrowDB-Connector/index.js b/Facebook-ArrowDB-Connector/index.js new file mode 100644 index 0000000..f1c6f07 --- /dev/null +++ b/Facebook-ArrowDB-Connector/index.js @@ -0,0 +1,17 @@ +exports = module.exports = require('./lib'); + +// if this is being run directly, then attempt to load it as +// server instead of module +if (module.id === '.') { + var fs = require('fs'), + path = require('path'), + appjs = path.join(__dirname, 'app.js'); + if (fs.existsSync(appjs)) { + try { + require(appjs); + } + catch (E) { + console.error(E); + } + } +} \ No newline at end of file diff --git a/Facebook-ArrowDB-Connector/lib/index.js b/Facebook-ArrowDB-Connector/lib/index.js new file mode 100644 index 0000000..73aaa03 --- /dev/null +++ b/Facebook-ArrowDB-Connector/lib/index.js @@ -0,0 +1,37 @@ +/* + Welcome to your new connector! + TODO: First things first, look at the "capabilities" array TODOs down below. + */ +var _ = require('lodash'), + semver = require('semver'); + +/** + * Creates your connector for Arrow. + */ +exports.create = function (Arrow) { + var min = '1.7.0'; + if (semver.lt(Arrow.Version || '0.0.1', min)) { + throw new Error('This connector requires at least version ' + min + ' of Arrow; please run `appc use latest`.'); + } + var Connector = Arrow.Connector, + Capabilities = Connector.Capabilities; + + return Connector.extend({ + filename: module.filename, + defaultConfig: require('fs').readFileSync(__dirname + '/../conf/example.config.js', 'utf8'), + capabilities: [ + Capabilities.ConnectsToADataSource, + + // TODO: Each of these capabilities is optional; add the ones you want, and delete the rest. + // (Hint: I've found it to be easiest to add these one at a time, running `appc run` for guidance.) + Capabilities.ValidatesConfiguration, + //Capabilities.ContainsModels, + Capabilities.GeneratesModels, + //Capabilities.CanCreate, + Capabilities.CanRetrieve, + //Capabilities.CanUpdate, + //Capabilities.CanDelete, + //Capabilities.AuthenticatesThroughConnector + ] + }); +}; diff --git a/Facebook-ArrowDB-Connector/lib/lifecycle/connect.js b/Facebook-ArrowDB-Connector/lib/lifecycle/connect.js new file mode 100644 index 0000000..198181f --- /dev/null +++ b/Facebook-ArrowDB-Connector/lib/lifecycle/connect.js @@ -0,0 +1,28 @@ +// TODO: Reference the module to connect to your data store. +var FB = require('fb'); + + +/** + * Connects to your data store; this connection can later be used by your connector's methods. + * @param next + */ +exports.connect = function (next) { + // Note: Our current context, aka "this", is a reference to your connector. + var self = this; + + connection = FB.extend({ "appId": this.config.appId, "appSecret": this.config.appSecret }); + self.logger.debug("Trying to validate our Facebook access_token by calling the /me endpoint..."); + FB.api('/me', { + fields: 'name,id', + access_token: this.config.accessToken + }, function (result) { + if(!result || result.error) { + self.logger.error("Facebook error: " + (result != null ? result.error : "")); + next('Got an error while validating the access_token on /me: ' + (result != null ? result.error : "")); + } else { + self.logger.info("Successfully validated our access_token ! Facebook response = " + JSON.stringify(result)); + next(); + } + }); + +}; diff --git a/Facebook-ArrowDB-Connector/lib/lifecycle/disconnect.js b/Facebook-ArrowDB-Connector/lib/lifecycle/disconnect.js new file mode 100644 index 0000000..bea4895 --- /dev/null +++ b/Facebook-ArrowDB-Connector/lib/lifecycle/disconnect.js @@ -0,0 +1,7 @@ +/** + * Disconnects from your data store. + * @param next + */ +exports.disconnect = function (next) { + next(); // allways succeed +}; diff --git a/Facebook-ArrowDB-Connector/lib/metadata/fetchMetadata.js b/Facebook-ArrowDB-Connector/lib/metadata/fetchMetadata.js new file mode 100644 index 0000000..303c96e --- /dev/null +++ b/Facebook-ArrowDB-Connector/lib/metadata/fetchMetadata.js @@ -0,0 +1,17 @@ +var Arrow = require('arrow'); + +/** + * Fetches metadata describing your connector's proper configuration. + * @param next + */ +exports.fetchMetadata = function fetchMetadata(next) { + next(null, { + fields: [ + Arrow.Metadata.Text({ + name: 'accessToken', + description: 'A Facebook access_token, that you can get with the "get-oauth-token.sh".', + required: true + }) + ] + }); +}; diff --git a/Facebook-ArrowDB-Connector/lib/methods/distinct.js b/Facebook-ArrowDB-Connector/lib/methods/distinct.js new file mode 100644 index 0000000..6df1b0b --- /dev/null +++ b/Facebook-ArrowDB-Connector/lib/methods/distinct.js @@ -0,0 +1,21 @@ +// TODO: Reference the module to connect to your data store. +var yourDataStore = /*require('your-data-store')*/{}; + +/** + * Performs a query and returns a distinct result set based on the field(s). + * @param {Arrow.Model} Model Model class to check. + * @param {String} field Comma-separated list of fields. + * @param {ArrowQueryOptions} [options] Query options. + * @param {Function} callback Callback passed an Error object (or null if successful) and the distinct values array. + */ +exports.distinct = function distinct(Model, field, options, callback) { + // TODO: Find the distinct results for this Model from your data store. + yourDataStore.distinct(field, options.where, function (err, results) { + if (err) { + return callback(err); + } + + // TODO: Return just the distinct values array. + callback(null, results); + }); +}; diff --git a/Facebook-ArrowDB-Connector/lib/methods/findAll.js b/Facebook-ArrowDB-Connector/lib/methods/findAll.js new file mode 100644 index 0000000..d203089 --- /dev/null +++ b/Facebook-ArrowDB-Connector/lib/methods/findAll.js @@ -0,0 +1,11 @@ +var FB = require('fb'), + Arrow = require('arrow'); + +/** + * Finds all model instances. A maximum of 1000 models are returned. + * @param {Arrow.Model} Model The model class being updated. + * @param {Function} callback Callback passed an Error object (or null if successful) and the models. + */ +exports.findAll = function findAll(Model, callback) { + Model.query({}, callback); +}; diff --git a/Facebook-ArrowDB-Connector/lib/methods/findByID.js b/Facebook-ArrowDB-Connector/lib/methods/findByID.js new file mode 100644 index 0000000..a074ea1 --- /dev/null +++ b/Facebook-ArrowDB-Connector/lib/methods/findByID.js @@ -0,0 +1,29 @@ +var FB = require('fb'); + +/** + * Finds a model instance using the primary key. + * @param {Arrow.Model} Model The model class being updated. + * @param {String} id ID of the model to find. + * @param {Function} callback Callback passed an Error object (or null if successful) and the found model. + */ +exports.findByID = function (Model, id, callback) { + self.logger.debug("--> findByID(" + Model.name + ", " + id + ")"); + + + // TODO: Find the instance with the provided id. + yourDataStore.findByID(id, function (err, result) { + if (err) { + return callback(err); + } + + // TODO: If nothing was found by this request: + if (!result) { + return callback(); + } + + // TODO: Otherwise, if all went well: + var instance = Model.instance(result, true); + instance.setPrimaryKey(String(result.id)); // Note: the primary key can be a number, too. + callback(null, instance); + }); +}; diff --git a/Facebook-ArrowDB-Connector/lib/methods/query.js b/Facebook-ArrowDB-Connector/lib/methods/query.js new file mode 100644 index 0000000..3c71659 --- /dev/null +++ b/Facebook-ArrowDB-Connector/lib/methods/query.js @@ -0,0 +1,74 @@ +var FB = require('fb'), + Arrow = require('arrow'), + _ = require('lodash'); + +/** + * Queries for particular model records. + * @param {Arrow.Model} Model The model class being updated. + * @param {ArrowQueryOptions} options Query options. + * @param {Function} callback Callback passed an Error object (or null if successful) and the model records. + * @throws {Error} Failed to parse query options. + */ +exports.query = function (Model, options, callback) { + var self = this; + var query = { + /** + * A dictionary of the fields to include, such as { first_name: 1 } + */ + //sel: Model.translateKeysForPayload(options.sel), + /** + * A dictionary of the fields to exclude, such as { last_name: 0 } + */ + //unsel: Model.translateKeysForPayload(options.unsel), + /** + * A dictionary of fields to search by, ignoring keys that aren't specified in our model, and including "id", + * such as { first_name: 'Daws%', last_name: 'Toth' } + */ + //where: _.pick(Model.translateKeysForPayload(options.where), Model.payloadKeys().concat(['id'])), + /** + * A dictionary of fields to order by, with a direction, such as { first_name: 1, last_name: -1 } where 1 is + * ascending and -1 is descending. + */ + //order: Model.translateKeysForPayload(options.order), + /** + * A number indicating how far to skip through the results before returning them, such as 0 or 100, as well + * as a limit on how many to return, such as 10 or 20. Alternatively, use options.page and options.per_page. + * Arrow translates these for you. + * + * For example, a skip of 50 and a limit of 10 is equivalent to a page of 5 and a per_page of 10. + */ + //skip: options.skip, + //limit: options.limit, + access_token: this.config.accessToken, + }; + + self.logger.debug("--> query(" + Model.name + ", " + JSON.stringify(options) + ")"); + self.logger.info("Fetching Facebook posts and likes for page '" + Model.private.name + "'..."); + FB.api("/" + Model.private.pageId + "/posts?fields=likes{id,name},id,message&limit=" + options.limit + "&offset=" + options.skip, query, function (result) { + if(!result || result.error) { + self.logger.error("Facebook error: " + (result != null ? result.error : "")); + callback('Got an error while Trying to find all likes of ' + Model.name + ": " + (result != null ? result.error : "")); + } else { + self.logger.info("Successfully found all likes for page '" + Model.private.name + "': " + JSON.stringify(result)); + var results = result.data; + var array = []; + for (var p = 0; p < results.length; p++) { + var post = results[p]; + var postId = post.id; + var postName = post.message; + + var likes = post.likes.data; + for (var l = 0; l < likes.length; l++) { + var like = likes[l]; + var instance = Model.instance({postId: postId, postName: postName, userId: like.id, userName: like.name}, true); + //instance.setPrimaryKey(String(results[c].id)); + array.push(instance); + } + } + + // Turn the array of instances in to a collection, and return it. + callback(null, new Arrow.Collection(Model, array)); + } + }); + +}; diff --git a/Facebook-ArrowDB-Connector/lib/schema/createModelsFromSchema.js b/Facebook-ArrowDB-Connector/lib/schema/createModelsFromSchema.js new file mode 100644 index 0000000..157fe24 --- /dev/null +++ b/Facebook-ArrowDB-Connector/lib/schema/createModelsFromSchema.js @@ -0,0 +1,40 @@ +var Arrow = require('arrow'), + _ = require('lodash'); + +/** + * Creates models from your schema (see "fetchSchema" for more information on the schema). + */ +exports.createModelsFromSchema = function () { + var self = this, + models = {}; + + // TODO: Iterate through the models in your schema. + Object.keys(self.metadata.schema.objects).forEach(function (modelName) { + var object = self.metadata.schema.objects[modelName], + fields = {}, private = null; + Object.keys(object).forEach(function (fieldName) { + + var field = object[fieldName]; + if (fieldName !== 'id') { + // TODO: Define the Arrow field definitions based on the schema. + fields[fieldName] = { + type: field.type || String, + required: field.required + }; + } + }); + + models[self.name + '/' + modelName] = Arrow.Model.extend(self.name + '/' + modelName, { + name: self.name + '/' + modelName, + autogen: !!self.config.modelAutogen, // Controls if APIs are automatically created for this model. + fields: fields, + connector: self, + logger: self.logger, + generated: true, + // cache Facebook attrs in the model to save a roundtrip + "private": self.metadata.schema.private[modelName] + }); + }); + + self.models = _.defaults(self.models || {}, models); +}; diff --git a/Facebook-ArrowDB-Connector/lib/schema/fetchSchema.js b/Facebook-ArrowDB-Connector/lib/schema/fetchSchema.js new file mode 100644 index 0000000..d15e971 --- /dev/null +++ b/Facebook-ArrowDB-Connector/lib/schema/fetchSchema.js @@ -0,0 +1,71 @@ +var FB = require('fb'); + +/** + * Fetches the schema for your connector. + * + * For example, your schema could look something like this: + * { + * objects: { + * person: { + * first_name: { + * type: 'string', + * required: true + * }, + * last_name: { + * type: 'string', + * required: false + * }, + * age: { + * type: 'number', + * required: false + * } + * } + * } + * } + * + * @param next + * @returns {*} + */ +exports.fetchSchema = function (next) { + var self = this; + // If we already have the schema, just return it. + if (this.metadata.schema) { + return next(null, this.metadata.schema); + } + + var action = "get the list of all Facebook pages managed by the current user"; + self.logger.debug("Trying to " + action + "..."); + FB.api('/me/accounts', { + access_token: this.config.accessToken + }, function (result) { + if(!result || result.error) { + self.logger.error("Could not " + action + ": " + (result != null ? result.error : "")); + next('Got an error while trying to ' + action + ': ' + (result != null ? result.error : "")); + } else { + var n = result.data.length; + self.logger.info("Successfully retrieved the list of all Facebook pages managed by the current user (" + n + " pages) !"); + self.logger.debug("Computing the schema from the facebook response..."); + var objects = {}; + var schemaCache = {}; + for (var i = 0; i < n; i++) { + var facebookPage = result.data[i]; + var pageName = "LikesOf" + camelize(facebookPage.name); + objects[pageName] = { "postId": { "type": "string", "required": "true" }, + "postName": { "type": "string", "required": "false" }, + "userId": { "type": "string", "required": "true" }, + "userName": { "type": "string", "required": "false" } }; + schemaCache[pageName] = { accessToken: facebookPage.access_token, pageId: facebookPage.id, name: facebookPage.name }; + self.logger.debug("New Schema: " + pageName); + } + var schema = { 'objects': objects, 'private': schemaCache }; + next(null, schema); + } + }); + +}; + +function camelize(str) { + return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) { + return letter.toUpperCase(); + }).replace(/\s+/g, ''); +} diff --git a/Facebook-ArrowDB-Connector/package.json b/Facebook-ArrowDB-Connector/package.json new file mode 100644 index 0000000..06f0155 --- /dev/null +++ b/Facebook-ArrowDB-Connector/package.json @@ -0,0 +1,35 @@ +{ + "name": "appc.facebook", + "description": "", + "version": "1.0.0", + "author": "Nicolas MASSE", + "license": "", + "framework": "none", + "keywords": [ + "appcelerator", + "arrow" + ], + "repository": {}, + "private": true, + "dependencies": { + "async": "^1.5.0", + "lodash": "^3.10.1", + "pkginfo": "^0.3.1", + "semver": "^5.0.3", + "fb": "^*" + }, + "devDependencies": { + "grunt": "^0.4.5", + "grunt-appc-js": "^1.0.19", + "grunt-contrib-clean": "^0.7.0", + "grunt-mocha-istanbul": "^3.0.1", + "istanbul": "^0.4.1", + "mocha": "^2.3.4", + "should": "^8.0.2", + "arrow": "^*" + }, + "scripts": { + "start": "appc run", + "test": "grunt" + } +}