Explorar el Código

companion,facebook: add support for facebook provider

ifedapoolarewaju hace 5 años
padre
commit
c3ea4d384f

+ 2 - 0
env.example.sh

@@ -8,6 +8,8 @@ export COMPANION_GOOGLE_KEY="***"
 export COMPANION_GOOGLE_SECRET="***"
 export COMPANION_INSTAGRAM_KEY="***"
 export COMPANION_INSTAGRAM_SECRET="***"
+export COMPANION_FACEBOOK_KEY="***"
+export COMPANION_FACEBOOK_SECRET="***"
 export EDGLY_KEY="***"
 export EDGLY_SECRET="***"
 export GITHUB_TOKEN="***"

+ 2 - 0
examples/dev/Dashboard.js

@@ -1,6 +1,7 @@
 const Uppy = require('@uppy/core/src')
 const Dashboard = require('@uppy/dashboard/src')
 const Instagram = require('@uppy/instagram/src')
+const Facebook = require('@uppy/facebook/src')
 const Dropbox = require('@uppy/dropbox/src')
 const GoogleDrive = require('@uppy/google-drive/src')
 const Url = require('@uppy/url/src')
@@ -35,6 +36,7 @@ module.exports = () => {
     .use(GoogleDrive, { target: Dashboard, companionUrl: 'http://localhost:3020' })
     .use(Instagram, { target: Dashboard, companionUrl: 'http://localhost:3020' })
     .use(Dropbox, { target: Dashboard, companionUrl: 'http://localhost:3020' })
+    .use(Facebook, { target: Dashboard, companionUrl: 'http://localhost:3020' })
     .use(Url, { target: Dashboard, companionUrl: 'http://localhost:3020' })
     .use(Webcam, { target: Dashboard })
     .use(Tus, { endpoint: TUS_ENDPOINT })

+ 4 - 0
packages/@uppy/companion/env_example

@@ -20,6 +20,10 @@ COMPANION_INSTAGRAM_KEY=
 COMPANION_INSTAGRAM_SECRET=
 COMPANION_INSTAGRAM_SECRET_FILE=
 
+COMPANION_FACEBOOK_KEY=
+COMPANION_FACEBOOK_SECRET=
+COMPANION_FACEBOOK_SECRET_FILE=
+
 COMPANION_AWS_KEY=
 COMPANION_AWS_SECRET=
 COMPANION_AWS_SECRET_FILE=

+ 5 - 0
packages/@uppy/companion/src/config/grant.js

@@ -17,6 +17,11 @@ module.exports = () => {
     instagram: {
       transport: 'session',
       callback: '/instagram/callback'
+    },
+    facebook: {
+      transport: 'session',
+      scope: ['email', 'user_photos'],
+      callback: '/facebook/callback'
     }
   }
 }

+ 4 - 4
packages/@uppy/companion/src/server/provider/drive/index.js

@@ -33,7 +33,7 @@ class Drive {
         this.client
           .query()
           .get('teamdrives')
-          .where({ fields: TEAM_DRIVE_FIELDS })
+          .qs({ fields: TEAM_DRIVE_FIELDS })
           .auth(options.token)
           .request((err, resp) => {
             if (err) {
@@ -57,7 +57,7 @@ class Drive {
       this.client
         .query()
         .get('files')
-        .where(where)
+        .qs(where)
         .auth(options.token)
         .request((err, resp) => {
           if (err || resp.statusCode !== 200) {
@@ -91,7 +91,7 @@ class Drive {
     return this.client
       .query()
       .get(`files/${id}`)
-      .where({ fields: DRIVE_FILE_FIELDS, supportsTeamDrives: true })
+      .qs({ fields: DRIVE_FILE_FIELDS, supportsTeamDrives: true })
       .auth(token)
       .request(done)
   }
@@ -100,7 +100,7 @@ class Drive {
     return this.client
       .query()
       .get(`files/${id}`)
-      .where({ alt: 'media', supportsTeamDrives: true })
+      .qs({ alt: 'media', supportsTeamDrives: true })
       .auth(token)
       .request()
       .on('data', onData)

+ 1 - 1
packages/@uppy/companion/src/server/provider/dropbox/index.js

@@ -94,7 +94,7 @@ class DropBox {
     this.client
       .post('files/list_folder')
       .options({ version: '2' })
-      .where(query)
+      .qs(query)
       .auth(token)
       .json({
         path: `${directory || ''}`

+ 56 - 0
packages/@uppy/companion/src/server/provider/facebook/adapter.js

@@ -0,0 +1,56 @@
+const querystring = require('querystring')
+
+exports.isFolder = (item) => {
+  return !!item.type
+}
+
+exports.getItemIcon = (item) => {
+  if (exports.isFolder(item)) {
+    return 'folder'
+  }
+  return exports.sortImages(item.images)[0].source
+}
+
+exports.getItemSubList = (item) => {
+  return item.data
+}
+
+exports.getItemName = (item) => {
+  return item.name || `${item.id} ${item.created_time}`
+}
+
+exports.getMimeType = (item) => {
+  return exports.isFolder(item) ? null : 'image/jpeg'
+}
+
+exports.getItemId = (item) => {
+  return `${item.id}`
+}
+
+exports.getItemRequestPath = (item) => {
+  return `${item.id}`
+}
+
+exports.getItemModifiedDate = (item) => {
+  return item.created_time
+}
+
+exports.getItemThumbnailUrl = (item) => {
+  return exports.isFolder(item) ? null : exports.sortImages(item.images)[0].source
+}
+
+exports.getNextPagePath = (data, currentQuery, currentPath) => {
+  if (!data.paging || !data.paging.cursors) {
+    return null
+  }
+  const query = {
+    ...currentQuery,
+    cursor: data.paging.cursors.after
+  }
+  return `${currentPath || ''}?${querystring.stringify(query)}`
+}
+
+exports.sortImages = (images) => {
+  // sort in ascending order of dimension
+  return images.slice().sort((a, b) => a.width - b.width)
+}

+ 150 - 0
packages/@uppy/companion/src/server/provider/facebook/index.js

@@ -0,0 +1,150 @@
+const request = require('request')
+const purest = require('purest')({ request })
+const utils = require('../../helpers/utils')
+const logger = require('../../logger')
+const adapter = require('./adapter')
+const AuthError = require('../error')
+
+class Facebook {
+  constructor (options) {
+    this.authProvider = options.provider = Facebook.authProvider
+    this.client = purest(options)
+  }
+
+  static get authProvider () {
+    return 'facebook'
+  }
+
+  list ({ directory, token, query = {} }, done) {
+    const qs = {
+      fields: 'name,cover_photo,created_time,type'
+    }
+
+    if (query.cursor) {
+      qs.after = query.cursor
+    }
+
+    let path = 'me/albums'
+    if (directory) {
+      path = `${directory}/photos`
+      qs.fields = 'icon,images,name,width,height,created_time'
+    }
+
+    this.client
+      .get(path)
+      .qs(qs)
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err || resp.statusCode !== 200) {
+          err = this._error(err, resp)
+          logger.error(err, 'provider.facebook.list.error')
+          return done(err)
+        } else {
+          this._getUsername(token, (err, username) => {
+            err ? done(err) : done(null, this.adaptData(body, username, directory, query))
+          })
+        }
+      })
+  }
+
+  _getUsername (token, done) {
+    this.client
+      .get('me')
+      .qs({ fields: 'email' })
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err || resp.statusCode !== 200) {
+          err = this._error(err, resp)
+          logger.error(err, 'provider.facebook.user.error')
+          return done(err)
+        } else {
+          done(null, body.email)
+        }
+      })
+  }
+
+  _getMediaUrl (body) {
+    const sortedImages = adapter.sortImages(body.images)
+    return sortedImages[sortedImages.length - 1].source
+  }
+
+  download ({ id, token }, onData) {
+    return this.client
+      .get(id)
+      .qs({ fields: 'images' })
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err) return logger.error(err, 'provider.facebook.download.error')
+        request(this._getMediaUrl(body))
+          .on('data', onData)
+          .on('end', () => onData(null))
+          .on('error', (err) => {
+            logger.error(err, 'provider.facebook.download.url.error')
+          })
+      })
+  }
+
+  thumbnail (_, done) {
+    // not implementing this because a public thumbnail from facebook will be used instead
+    const err = new Error('call to thumbnail is not implemented')
+    logger.error(err, 'provider.facebook.thumbnail.error')
+    return done(err)
+  }
+
+  size ({ id, token }, done) {
+    return this.client
+      .get(id)
+      .qs({ fields: 'images' })
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err || resp.statusCode !== 200) {
+          err = this._error(err, resp)
+          logger.error(err, 'provider.facebook.size.error')
+          return done(err)
+        }
+
+        utils.getURLMeta(this._getMediaUrl(body))
+          .then(({ size }) => done(null, size))
+          .catch((err) => {
+            logger.error(err, 'provider.facebook.size.error')
+            done()
+          })
+      })
+  }
+
+  adaptData (res, username, directory, currentQuery) {
+    const data = { username: username, items: [] }
+    const items = adapter.getItemSubList(res)
+    items.forEach((item) => {
+      data.items.push({
+        isFolder: adapter.isFolder(item),
+        icon: adapter.getItemIcon(item),
+        name: adapter.getItemName(item),
+        mimeType: adapter.getMimeType(item),
+        id: adapter.getItemId(item),
+        thumbnail: adapter.getItemThumbnailUrl(item),
+        requestPath: adapter.getItemRequestPath(item),
+        modifiedDate: adapter.getItemModifiedDate(item)
+      })
+    })
+
+    data.nextPagePath = adapter.getNextPagePath(res, currentQuery, directory)
+    return data
+  }
+
+  _error (err, resp) {
+    if (resp) {
+      if (resp.body && resp.body.error.code === 190) {
+        // Invalid OAuth 2.0 Access Token
+        return new AuthError()
+      }
+
+      const msg = resp.body && resp.body.error ? resp.body.error.message : ''
+      return new Error(`request to ${this.authProvider} returned status: ${resp.statusCode}, message: ${msg}`)
+    }
+
+    return err
+  }
+}
+
+module.exports = Facebook

+ 2 - 1
packages/@uppy/companion/src/server/provider/index.js

@@ -6,6 +6,7 @@ const config = require('@purest/providers')
 const dropbox = require('./dropbox')
 const drive = require('./drive')
 const instagram = require('./instagram')
+const facebook = require('./facebook')
 const { getURLBuilder } = require('../helpers/utils')
 const logger = require('../logger')
 
@@ -91,7 +92,7 @@ module.exports.getProviderMiddleware = (providers) => {
  * @return {Object.<string, typeof Provider>}
  */
 module.exports.getDefaultProviders = () => {
-  return { dropbox, drive, instagram }
+  return { dropbox, drive, instagram, facebook }
 }
 
 /**

+ 7 - 26
packages/@uppy/companion/src/server/provider/instagram/index.js

@@ -19,7 +19,7 @@ class Instagram {
     const cursor = query.cursor || query.max_id
     const qs = cursor ? { max_id: cursor } : {}
     this.client
-      .select(`users/self/media/${directory}`)
+      .get(`users/self/media/${directory}`)
       .qs(qs)
       .auth(token)
       .request((err, resp, body) => {
@@ -37,7 +37,7 @@ class Instagram {
 
   _getUsername (token, done) {
     this.client
-      .select('users/self')
+      .get('users/self')
       .auth(token)
       .request((err, resp, body) => {
         if (err || resp.statusCode !== 200) {
@@ -81,30 +81,11 @@ class Instagram {
       })
   }
 
-  thumbnail ({ id, token }, done) {
-    return this.client
-      .get(`media/${id}`)
-      .auth(token)
-      .request((err, resp, body) => {
-        if (err) {
-          err = this._error(err, resp)
-          logger.error(err, 'provider.instagram.thumbnail.error')
-          return done(err)
-        }
-
-        request(body.data.images.thumbnail.url)
-          .on('response', (resp) => {
-            if (resp.statusCode !== 200) {
-              err = this._error(null, resp)
-              logger.error(err, 'provider.instagram.thumbnail.error')
-              return done(err)
-            }
-            done(null, resp)
-          })
-          .on('error', (err) => {
-            logger.error(err, 'provider.instagram.thumbnail.error')
-          })
-      })
+  thumbnail (_, done) {
+    // not implementing this because a public thumbnail from instagram will be used instead
+    const err = new Error('call to thumbnail is not implemented')
+    logger.error(err, 'provider.instagram.thumbnail.error')
+    return done(err)
   }
 
   size ({ id, token, query = {} }, done) {

+ 4 - 0
packages/@uppy/companion/src/standalone/helper.js

@@ -41,6 +41,10 @@ const getConfigFromEnv = () => {
         key: process.env.COMPANION_INSTAGRAM_KEY,
         secret: getSecret('COMPANION_INSTAGRAM_SECRET')
       },
+      facebook: {
+        key: process.env.COMPANION_FACEBOOK_KEY,
+        secret: getSecret('COMPANION_FACEBOOK_SECRET')
+      },
       s3: {
         key: process.env.COMPANION_AWS_KEY,
         secret: getSecret('COMPANION_AWS_SECRET'),

+ 1 - 1
packages/@uppy/companion/test/__mocks__/purest.js

@@ -2,7 +2,7 @@ const fs = require('fs')
 
 class MockPurest {
   constructor (opts) {
-    const methodsToMock = ['query', 'select', 'where', 'auth', 'get', 'put', 'post', 'options', 'json']
+    const methodsToMock = ['query', 'select', 'where', 'qs', 'auth', 'get', 'put', 'post', 'options', 'json']
     methodsToMock.forEach((item) => {
       this[item] = () => this
     })

+ 21 - 0
packages/@uppy/facebook/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Transloadit
+
+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.

+ 40 - 0
packages/@uppy/facebook/README.md

@@ -0,0 +1,40 @@
+# @uppy/facebook
+
+<img src="https://uppy.io/images/logos/uppy-dog-head-arrow.svg" width="120" alt="Uppy logo: a superman puppy in a pink suit" align="right">
+
+<a href="https://www.npmjs.com/package/@uppy/facebook"><img src="https://img.shields.io/npm/v/@uppy/facebook.svg?style=flat-square"></a>
+<a href="https://travis-ci.org/transloadit/uppy"><img src="https://img.shields.io/travis/transloadit/uppy/master.svg?style=flat-square" alt="Build Status"></a>
+
+A description of this plugin or module goes here.
+
+Uppy is being developed by the folks at [Transloadit](https://transloadit.com), a versatile file encoding service.
+
+## Example
+
+```js
+const Uppy = require('@uppy/core')
+const Facebook = require('@uppy/facebook')
+
+const uppy = Uppy()
+uppy.use(Facebook, {
+  // Options
+})
+```
+
+## Installation
+
+```bash
+$ npm install @uppy/facebook --save
+```
+
+We recommend installing from npm and then using a module bundler such as [Webpack](https://webpack.js.org/), [Browserify](http://browserify.org/) or [Rollup.js](http://rollupjs.org/).
+
+Alternatively, you can also use this plugin in a pre-built bundle from Transloadit's CDN: Edgly. In that case `Uppy` will attach itself to the global `window.Uppy` object. See the [main Uppy documentation](https://uppy.io/docs/#Installation) for instructions.
+
+## Documentation
+
+Documentation for this plugin can be found on the [Uppy website](https://uppy.io/docs/facebook).
+
+## License
+
+[The MIT License](./LICENSE).

+ 31 - 0
packages/@uppy/facebook/package.json

@@ -0,0 +1,31 @@
+{
+  "name": "@uppy/facebook",
+  "description": "Import files from Facbook, into Uppy.",
+  "version": "1.2.0",
+  "license": "MIT",
+  "main": "lib/index.js",
+  "types": "types/index.d.ts",
+  "keywords": [
+    "file uploader",
+    "uppy",
+    "uppy-plugin",
+    "facebook"
+  ],
+  "homepage": "https://uppy.io",
+  "bugs": {
+    "url": "https://github.com/transloadit/uppy/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/transloadit/uppy.git"
+  },
+  "dependencies": {
+    "@uppy/companion-client": "file:../companion-client",
+    "@uppy/provider-views": "file:../provider-views",
+    "@uppy/utils": "file:../utils",
+    "preact": "8.2.9"
+  },
+  "peerDependencies": {
+    "@uppy/core": "^1.0.0"
+  }
+}

+ 71 - 0
packages/@uppy/facebook/src/index.js

@@ -0,0 +1,71 @@
+const { Plugin } = require('@uppy/core')
+const { Provider } = require('@uppy/companion-client')
+const ProviderViews = require('@uppy/provider-views')
+const { h } = require('preact')
+
+module.exports = class Facebook extends Plugin {
+  static VERSION = require('../package.json').version
+
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.id = this.opts.id || 'Facebook'
+    Provider.initPlugin(this, opts)
+    this.title = this.opts.title || 'Facebook'
+    this.icon = () => (
+      <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+        width="266.893px" height="266.895px" viewBox="0 0 266.893 266.895" enable-background="new 0 0 266.893 266.895">
+        <path id="Blue_1_" fill="#3C5A99" d="M248.082,262.307c7.854,0,14.223-6.369,14.223-14.225V18.812
+          c0-7.857-6.368-14.224-14.223-14.224H18.812c-7.857,0-14.224,6.367-14.224,14.224v229.27c0,7.855,6.366,14.225,14.224,14.225
+          H248.082z" />
+        <path id="f" fill="#FFFFFF" d="M182.409,262.307v-99.803h33.499l5.016-38.895h-38.515V98.777c0-11.261,3.127-18.935,19.275-18.935
+          l20.596-0.009V45.045c-3.562-0.474-15.788-1.533-30.012-1.533c-29.695,0-50.025,18.126-50.025,51.413v28.684h-33.585v38.895h33.585
+          v99.803H182.409z" />
+      </svg>
+    )
+
+    this.provider = new Provider(uppy, {
+      companionUrl: this.opts.companionUrl,
+      serverHeaders: this.opts.serverHeaders,
+      storage: this.opts.storage,
+      provider: 'facebook',
+      pluginId: this.id
+    })
+
+    this.onFirstRender = this.onFirstRender.bind(this)
+    this.render = this.render.bind(this)
+  }
+
+  install () {
+    this.view = new ProviderViews(this, {
+      provider: this.provider
+    })
+    // Set default state for Dropbox
+    this.setPluginState({
+      authenticated: false,
+      files: [],
+      folders: [],
+      directories: [],
+      activeRow: -1,
+      filterInput: '',
+      isSearchVisible: false
+    })
+
+    const target = this.opts.target
+    if (target) {
+      this.mount(target, this)
+    }
+  }
+
+  uninstall () {
+    this.view.tearDown()
+    this.unmount()
+  }
+
+  onFirstRender () {
+    return this.view.getFolder()
+  }
+
+  render (state) {
+    return this.view.render(state)
+  }
+}

+ 21 - 0
packages/@uppy/facebook/types/index.d.ts

@@ -0,0 +1,21 @@
+import Uppy = require('@uppy/core');
+import CompanionClient = require('@uppy/companion-client');
+
+declare module Facebook {
+  interface FacebookOptions extends Uppy.PluginOptions, CompanionClient.ProviderOptions {
+    companionUrl: string;
+    companionAllowedHosts: string | RegExp | Array<string | RegExp>;
+  }
+}
+
+declare class Facebook extends Uppy.Plugin {
+  constructor(uppy: Uppy.Uppy, opts: Partial<Facebook.FacebookOptions>);
+}
+
+export = Facebook;
+
+declare module '@uppy/core' {
+  export interface Uppy {
+    use(pluginClass: typeof Facebook, opts: Partial<Facebook.FacebookOptions>): Uppy.Uppy;
+  }
+}

+ 11 - 0
website/src/docs/companion.md

@@ -24,6 +24,7 @@ As of now, Companion is integrated to work with:
 - Google Drive
 - Dropbox
 - Instagram
+- Facebook
 - Remote URLs
 - Amazon S3
 
@@ -178,6 +179,12 @@ export COMPANION_INSTAGRAM_SECRET="YOUR INSTAGRAM SECRET"
 # specifying a secret file will override a directly set secret
 export COMPANION_INSTAGRAM_SECRET_FILE="PATH/TO/INSTAGRAM/SECRET/FILE"
 
+# to enable Facebook
+export COMPANION_FACEBOOK_KEY="YOUR FACEBOOK KEY"
+export COMPANION_FACEBOOK_SECRET="YOUR FACEBOOK SECRET"
+# specifying a secret file will override a directly set secret
+export COMPANION_FACEBOOK_SECRET_FILE="PATH/TO/FACEBOOK/SECRET/FILE"
+
 # to enable S3
 export COMPANION_AWS_KEY="YOUR AWS KEY"
 export COMPANION_AWS_SECRET="YOUR AWS SECRET"
@@ -218,6 +225,10 @@ See [env.example.sh](https://github.com/transloadit/uppy/blob/master/env.example
       key: "***",
       secret: "***"
     },
+    facebook: {
+      key: "***",
+      secret: "***"
+    },
     s3: {
       getKey: (req, filename) => filename,
       key: "***",