'use strict'
const querystring = require('querystring')
const promisify = require('./promisify')
const executeRequest = require('./executeRequest')
const GOOGLE_AUTH_ENDPOINT = 'https://accounts.google.com/o/oauth2/auth'
const GOOGLE_API_HOST = 'https://www.googleapis.com'
const GOOGLE_API_PATH = '/oauth2/v3/token'
const GOOGLE_API_PATH_NEW = '/oauth2/v4/token'
const PICASA_SCOPE = 'https://picasaweb.google.com/data'
const PICASA_API_FEED_PATH = '/feed/api/user/default'
const PICASA_API_ENTRY_PATH = '/entry/api/user/default'
const FETCH_AS_JSON = 'json'
/** Main class */
class Picasa {
/**
* Creates an instance of Picasa.
*/
constructor() {
this.executeRequest = executeRequest
this.getAccessToken = getAccessToken.bind(this)
}
/**
* Get Photos
* @param {string} accessToken - See {@link Picasa#getTokens}
* @param {object} options - Can be empty object
* @param {integer} options.maxResults - By default get all photos
* @param {string} options.albumId - By default all photos are selected
* @param {function} callback - (error, response). If not provided, a promise will be returned
* @returns {Promise}
*/
getPhotos() {
return promisify.bind(this)(getPhotos, arguments)
}
/**
* Create Photos
* @param {string} accessToken - See {@link Picasa#getTokens}
* @param {string} albumId
* @param {object} photoData - Photo's propperties
* @param {string} photoData.title
* @param {string} photoData.summary
* @param {string} photoData.contentType - image/bmp, image/gif, image/png
* @param {blob} photoData.binary - Blob binary
* @param {function} callback - (error, response). If not provided, a promise will be returned
* @returns {Promise}
*/
postPhoto() {
return promisify.bind(this)(postPhoto, arguments)
}
/**
* Delete Photo
* @param {string} accessToken - See {@link Picasa#getTokens}
* @param {string} albumId
* @param {string} photoId
* @param {function} callback - (error, response). If not provided, a promise will be returned
* @returns {Promise}
*/
deletePhoto() {
return promisify.bind(this)(deletePhoto, arguments)
}
/**
* Get all Albums
* @param {string} accessToken - See {@link Picasa#getTokens}
* @param {object} options - Can be empty object
* @param {integer} options.TODO - TODO
* @param {function} callback - (error, response). If not provided, a promise will be returned
* @returns {Promise}
*/
getAlbums() {
return promisify.bind(this)(getAlbums, arguments)
}
/**
* Create an albums
* @param {string} accessToken - See {@link Picasa#getTokens}
* @param {object} albumData - Can be empty object
* @param {string} albumData.title
* @param {string} albumData.summary
* @param {function} callback - (error, response). If not provided, a promise will be returned
* @returns {Promise}
*/
createAlbum() {
return promisify.bind(this)(createAlbum, arguments)
}
/**
* Get access token and refresh token
* @param {object} config - Get config here: {@link https://console.developers.google.com/home/dashboard} (API Manager > Credentials)
* @param {string} config.clientId
* @param {string} config.redirectURI - URL that user was redirected. After google displays a consent screen to the user, user will be redirect to this URL with a `code` in the URL
* @param {string} config.clientSecret
* @param {string} code - Get code from URL param, when user is redirected from authURL. See {@link Picasa#getAuthURL}
* @param {function} callback - (error, response{accessToken, refreshToken}). If not provided, a promise will be returned
* @returns {Promise} - Object{accessToken, refreshToken}
*/
getTokens() {
return promisify.bind(this)(getTokens, arguments)
}
/**
* Renews access token
* @param {object} config - Get config here: {@link https://console.developers.google.com/home/dashboard} (API Manager > Credentials)
* @param {string} config.clientId
* @param {string} config.redirectURI - URL that user was redirected. After google displays a consent screen to the user, user will be redirect to this URL with a `code` in the URL
* @param {string} config.clientSecret
* @param {string} refreshToken - The refreshToken is retrived after getTokens is executed. See {@link Picasa#getTokens}
* @param {function} callback - (error, response). If not provided, a promise will be returned
* @returns {Promise}
*/
renewAccessToken() {
return promisify.bind(this)(renewAccessToken, arguments)
}
/**
* Get Auth URL. Redirect user to this URL to get code. The code will be used later for {@link Picasa#getTokens}
* @param {object} config - Get config here: https://console.developers.google.com/home/dashboard (API Manager > Credentials)
* @param {string} config.clientId
* @param {string} config.redirectURI - URL to user will be redirected. After google displays a consent screen to the user, user will be redirect to this URL with a `code` in the URL
* @param {function} callback - (error, response). If not provided, a promise will be returned
* @returns {Promise}
*/
getAuthURL(config) {
const authenticationParams = {
access_type: 'offline',
scope: `${PICASA_SCOPE}`,
response_type: 'code',
client_id: config.clientId,
redirect_uri: config.redirectURI
}
const authenticationQuery = querystring.stringify(authenticationParams)
return `${GOOGLE_AUTH_ENDPOINT}?${authenticationQuery}`
}
}
function getAlbums(accessToken, options, callback) {
const accessTokenParams = {
alt: FETCH_AS_JSON,
access_token: accessToken
}
options = options || {}
const requestQuery = querystring.stringify(accessTokenParams)
const requestOptions = {
url: `${PICASA_SCOPE}${PICASA_API_FEED_PATH}?${requestQuery}`,
headers: {
'GData-Version': '2'
}
}
this.executeRequest('get', requestOptions, (error, body) => {
if (error) return callback(error)
const albums = body.feed.entry.map(
entry => parseEntry(entry, albumSchema)
)
callback(null, albums)
})
}
function deletePhoto(accessToken, albumId, photoId, callback) {
const requestQuery = querystring.stringify({
alt: FETCH_AS_JSON,
access_token: accessToken
})
const requestOptions = {
url: `${PICASA_SCOPE}${PICASA_API_ENTRY_PATH}/albumid/${albumId}/photoid/${photoId}?${requestQuery}`,
headers: {
'If-Match': '*'
}
}
this.executeRequest('del', requestOptions, callback)
}
function createAlbum(accessToken, albumData, callback) {
const requestQuery = querystring.stringify({
alt: FETCH_AS_JSON,
access_token: accessToken
})
const albumInfoAtom = `<entry xmlns='http://www.w3.org/2005/Atom'
xmlns:media='http://search.yahoo.com/mrss/'
xmlns:gphoto='http://schemas.google.com/photos/2007'>
<title type='text'>${albumData.title}</title>
<summary type='text'>${albumData.summary}</summary>
<gphoto:access>private</gphoto:access>
<category scheme='http://schemas.google.com/g/2005#kind'
term='http://schemas.google.com/photos/2007#album'></category>
</entry>`
const requestOptions = {
url: `${PICASA_SCOPE}${PICASA_API_FEED_PATH}?${requestQuery}`,
body: albumInfoAtom,
headers: { 'Content-Type': 'application/atom+xml' }
}
this.executeRequest('post', requestOptions, (error, body) => {
if (error) return callback(error)
const album = parseEntry(body.entry, albumSchema)
callback(error, album)
})
}
function postPhoto(accessToken, albumId, photoData, callback) {
const requestQuery = querystring.stringify({
alt: FETCH_AS_JSON,
access_token: accessToken
})
const photoInfoAtom = `<entry xmlns="http://www.w3.org/2005/Atom">
<title>${photoData.title}</title>
<summary>${photoData.summary}</summary>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/photos/2007#photo"/>
</entry>`
const requestOptions = {
url: `${PICASA_SCOPE}${PICASA_API_FEED_PATH}/albumid/${albumId}?${requestQuery}`,
multipart: [
{ 'Content-Type': 'application/atom+xml', body: photoInfoAtom },
{ 'Content-Type': photoData.contentType, body: photoData.binary }
]
}
this.executeRequest('post', requestOptions, (error, body) => {
if (error) return callback(error)
const photo = parseEntry(body.entry, photoSchema)
callback(error, photo)
})
}
function getPhotos(accessToken, options, callback) {
const accessTokenParams = {
alt: FETCH_AS_JSON,
kind: 'photo',
access_token: accessToken
}
options = options || {}
if (options.maxResults) accessTokenParams['max-results'] = options.maxResults
if (options.startIndex) accessTokenParams['start-index'] = options.startIndex
if (options.imgMax) accessTokenParams['imgmax'] = options.imgMax
const albumPart = options.albumId ? `/albumid/${options.albumId}` : ''
const requestQuery = querystring.stringify(accessTokenParams)
const requestOptions = {
url: `${PICASA_SCOPE}${PICASA_API_FEED_PATH}${albumPart}?${requestQuery}`,
headers: {
'GData-Version': '2'
}
}
this.executeRequest('get', requestOptions, (error, body) => {
if (error) return callback(error)
const photos = body.feed.entry.map(
entry => parseEntry(entry, photoSchema)
)
callback(null, photos)
})
}
const albumSchema = {
'media$group.media$thumbnail': 'thumbnail',
'gphoto$id': 'id',
'gphoto$name': 'name',
'gphoto$numphotos': 'num_photos',
'published': 'published',
'title': 'title',
'summary': 'summary',
'gphoto$location': 'location',
'gphoto$nickname': 'nickname',
'rights': 'rights',
'gphoto$access': 'access'
}
const photoSchema = {
'gphoto$id': 'id',
'gphoto$albumid': 'album_id',
'gphoto$access': 'access',
'gphoto$width': 'width',
'gphoto$height': 'height',
'gphoto$size': 'size',
'gphoto$checksum': 'checksum',
'gphoto$timestamp': 'timestamp',
'gphoto$imageVersion': 'image_version',
'gphoto$commentingEnabled': 'commenting_enabled',
'gphoto$commentCount': 'comment_count',
'content': 'content',
'title': 'title',
'summary': 'summary'
}
function parseEntry(entry, schema) {
let photo = {}
Object.keys(schema).forEach(schemaKey => {
const key = schema[schemaKey]
if (key) {
const value = extractValue(entry, schemaKey, key)
photo[key] = value
}
})
return photo
}
function extractValue(entry, schemaKey) {
if (schemaKey.indexOf('.') !== -1) {
const tempKey = schemaKey.split('.')[0]
return extractValue(checkParam(entry[tempKey]), schemaKey.replace(`${tempKey}.`, ''))
}
return checkParam(entry[schemaKey])
}
function getAuthURL(config) {
const authenticationParams = {
access_type: 'offline',
scope: `${PICASA_SCOPE}`,
response_type: 'code',
client_id: config.clientId,
redirect_uri: config.redirectURI
}
const authenticationQuery = querystring.stringify(authenticationParams)
return `${GOOGLE_AUTH_ENDPOINT}?${authenticationQuery}`
}
/**
* Get access token. To be deprecated soon. Use {@link Picasa#getTokens} instead
* @param {object} config - Get config here: {@link https://console.developers.google.com/home/dashboard} (API Manager > Credentials)
* @param {string} config.clientId
* @param {string} config.redirectURI - URL that user was redirected. After google displays a consent screen to the user, user will be redirect to this URL with a `code` in the URL
* @param {string} config.clientSecret
* @param {string} code - Get code from URL param, when user is redirected from authURL. See {@link Picasa#getAuthURL}
* @param {function} callback - (error, response{accessToken, refreshToken}). If not provided, a promise will be returned
* @returns {Promise} - String accessToken
*/
function getAccessToken(config, code, callback) {
const accessTokenParams = {
grant_type: 'authorization_code',
code: code,
redirect_uri: config.redirectURI,
client_id: config.clientId,
client_secret: config.clientSecret
}
const requestQuery = querystring.stringify(accessTokenParams)
const options = {
url: `${GOOGLE_API_HOST}${GOOGLE_API_PATH}?${requestQuery}`
}
this.executeRequest('post', options, (error, body) => {
if (error) return callback(error)
callback(null, body.access_token, body.refresh_token)
})
}
function getTokens(config, code, callback) {
return getAccessToken(config, code, (error, accessToken, refreshToken) => {
if (error) return callback(error)
const tokens = {
accessToken,
refreshToken,
}
callback(null, tokens)
})
}
function renewAccessToken(config, refresh_token, callback) {
const refreshTokenParams = {
grant_type: 'refresh_token',
client_id: config.clientId,
client_secret: config.clientSecret,
refresh_token: refresh_token
}
const requestQuery = querystring.stringify(refreshTokenParams)
const options = {
url: `${GOOGLE_API_HOST}${GOOGLE_API_PATH_NEW}?${requestQuery}`
}
this.executeRequest('post', options, (error, body) => {
if (error) return callback(error)
callback(null, body.access_token)
})
}
function checkParam(param) {
if (param === undefined) return ''
else if (isValidType(param)) return param
else if (isValidType(param['$t'])) return param['$t']
else return param
}
function isValidType(value) {
return typeof value === 'string' || typeof value === 'number'
}
module.exports = Picasa