22 changed files with 541 additions and 0 deletions
@ -0,0 +1,2 @@ |
|||
facebook.* |
|||
node_modules |
|||
@ -0,0 +1,2 @@ |
|||
node_modules |
|||
logs |
|||
@ -0,0 +1 @@ |
|||
# appc.facebook |
|||
@ -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(); |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"type":"connector", |
|||
"group":"arrow", |
|||
"dependencies": { |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
local.js |
|||
*.local.js |
|||
deploy.json |
|||
@ -0,0 +1,3 @@ |
|||
local.js |
|||
*.local.js |
|||
deploy.json |
|||
@ -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: { |
|||
} |
|||
}; |
|||
@ -0,0 +1,7 @@ |
|||
module.exports = { |
|||
connectors: { |
|||
'appc.facebook': { |
|||
'accessToken': '<your facebook access_token here>' |
|||
} |
|||
} |
|||
}; |
|||
@ -0,0 +1,23 @@ |
|||
#!/bin/bash |
|||
|
|||
redirect_uri="http://localhost:8888/" |
|||
|
|||
if [ $# -ne 2 ]; then |
|||
echo "Usage: $0 <facebook appId> <facebook appSecret> " |
|||
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 <<EOF |
|||
HTTP 1.0 200 OK |
|||
Content-Type: text/html |
|||
|
|||
<html><body><h1>OK</h1></body></html> |
|||
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 |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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
|
|||
] |
|||
}); |
|||
}; |
|||
@ -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(); |
|||
} |
|||
}); |
|||
|
|||
}; |
|||
@ -0,0 +1,7 @@ |
|||
/** |
|||
* Disconnects from your data store. |
|||
* @param next |
|||
*/ |
|||
exports.disconnect = function (next) { |
|||
next(); // allways succeed
|
|||
}; |
|||
@ -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 |
|||
}) |
|||
] |
|||
}); |
|||
}; |
|||
@ -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); |
|||
}); |
|||
}; |
|||
@ -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); |
|||
}; |
|||
@ -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); |
|||
}); |
|||
}; |
|||
@ -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)); |
|||
} |
|||
}); |
|||
|
|||
}; |
|||
@ -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); |
|||
}; |
|||
@ -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, ''); |
|||
} |
|||
@ -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" |
|||
} |
|||
} |
|||
Loading…
Reference in new issue