Working with Google Apis

Hello, please help me to make google apis and meteor work. So I set up accounts-ui accounts-password accounts-google. I took the needed scopes with my accounts-config.js

import { Accounts } from 'meteor/accounts-base'

Accounts.ui.config({requestPermissions: {
  google: [
    'https://www.googleapis.com/auth/userinfo.email',
    'https://www.googleapis.com/auth/userinfo.profile',
    'https://mail.google.com/',
    'https://www.googleapis.com/auth/gmail.compose',
    'https://www.googleapis.com/auth/gmail.insert',
    'https://www.googleapis.com/auth/gmail.labels',
    'https://www.googleapis.com/auth/gmail.metadata',
    'https://www.googleapis.com/auth/gmail.modify',
    'https://www.googleapis.com/auth/gmail.readonly',
    'https://www.googleapis.com/auth/gmail.send',
    'https://www.googleapis.com/auth/gmail.settings.basic',
    'https://www.googleapis.com/auth/gmail.settings.sharing'
  ]
},
  requestOfflineToken: {
    google: true
  }
})

On my server I have a method like this

import { Meteor } from 'meteor/meteor'
import { HTTP } from 'meteor/http'

Meteor.startup(() => {
  // Meteor.call('createDraft')


  Meteor.methods({
    'createDraft': function () {
      console.log(this.userId)

      const user = Meteor.users.findOne(this.userId)
      const email = user.services.google.email
      console.log(email)

      const dataObject = {
        'message': {
          'raw': CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse('Hello, World!'))
        }
      }
      HTTP.post('https://www.googleapis.com/upload/gmail/v1/users/' + encodeURIComponent(email) + '/drafts', dataObject, (error, result) => {
        if (error) {
          console.log('err', error)
        }
        if (result) {
          console.log('res', result)
        }
      })
    }
  })
})

On the client I have a component like this

import React, { Component } from 'react'

class Test extends Component {
  createDraftClick () {
    Meteor.call('createDraft', () => console.log('called createDraft'))
  }

  render () {
    return (
      <div className="btn btn-primary" onClick={this.createDraftClick.bind(this)}>Create Draft</div>
    )
  }
}

export default Test

When I click on it and my method is called I get the following response

err { [Error: failed [401] {  "error": {   "errors": [    {     "domain": "global",     "reason": "required",     "message": "Login Required",     "locationType": "header",     "location": "Authorization"    }   ],   "code": 401,   "message": "Login Required"  } } ]
I20161219-17:02:07.904(4)?   response: 
I20161219-17:02:07.904(4)?    { statusCode: 401,
I20161219-17:02:07.905(4)?      content: '{\n "error": {\n  "errors": [\n   {\n    "domain": "global",\n    "reason": "required",\n    "message": "Login Required",\n    "locationType": "header",\n    "location": "Authorization"\n   }\n  ],\n  "code": 401,\n  "message": "Login Required"\n }\n}\n',
I20161219-17:02:07.907(4)?      headers: 
I20161219-17:02:07.908(4)?       { 'x-guploader-uploadid': 'AEnB2UrTNWqenpV_aT_KKab8HA71eHYdc3t8pjIzo6e_JyW4OWz3fIopa6hVpCmDK5nTM0VE8z3UOam8bSNKQNatKjLHHN_zNPFLQl_9eet8VNXO-3yNBYU',
I20161219-17:02:07.908(4)?         vary: 'Origin, X-Origin',
I20161219-17:02:07.913(4)?         'www-authenticate': 'Bearer realm="https://accounts.google.com/"',
I20161219-17:02:07.914(4)?         'content-type': 'application/json; charset=UTF-8',
I20161219-17:02:07.914(4)?         'content-length': '238',
I20161219-17:02:07.916(4)?         date: 'Mon, 19 Dec 2016 13:02:07 GMT',
I20161219-17:02:07.917(4)?         server: 'UploadServer',
I20161219-17:02:07.917(4)?         'alt-svc': 'quic=":443"; ma=2592000; v="35,34"',
I20161219-17:02:07.917(4)?         connection: 'close' },
I20161219-17:02:07.919(4)?      data: { error: [Object] } } }
I20161219-17:02:07.926(4)? res { statusCode: 401,
I20161219-17:02:07.926(4)?   content: '{\n "error": {\n  "errors": [\n   {\n    "domain": "global",\n    "reason": "required",\n    "message": "Login Required",\n    "locationType": "header",\n    "location": "Authorization"\n   }\n  ],\n  "code": 401,\n  "message": "Login Required"\n }\n}\n',
I20161219-17:02:07.926(4)?   headers: 
I20161219-17:02:07.928(4)?    { 'x-guploader-uploadid': 'AEnB2UrTNWqenpV_aT_KKab8HA71eHYdc3t8pjIzo6e_JyW4OWz3fIopa6hVpCmDK5nTM0VE8z3UOam8bSNKQNatKjLHHN_zNPFLQl_9eet8VNXO-3yNBYU',
I20161219-17:02:07.928(4)?      vary: 'Origin, X-Origin',
I20161219-17:02:07.929(4)?      'www-authenticate': 'Bearer realm="https://accounts.google.com/"',
I20161219-17:02:07.930(4)?      'content-type': 'application/json; charset=UTF-8',
I20161219-17:02:07.931(4)?      'content-length': '238',
I20161219-17:02:07.931(4)?      date: 'Mon, 19 Dec 2016 13:02:07 GMT',
I20161219-17:02:07.932(4)?      server: 'UploadServer',
I20161219-17:02:07.933(4)?      'alt-svc': 'quic=":443"; ma=2592000; v="35,34"',
I20161219-17:02:07.933(4)?      connection: 'close' },
I20161219-17:02:07.933(4)?   data: { error: { errors: [Object], code: 401, message: 'Login Required' } } }

But I am signed in with my google account. Why am I getting this error?
I also tried to use atmosphere package GoogleApi but I get the same error. Please help dear sirs. Thank you in advance.

We have used google apis and it worked from first go. First, did you log in with Google to Meteor? Check profile.services.google in your user doc. Check the token and expiry make sure they are active.

Second, you are using Accounts.ui (which we don’t use so not sure of it), are you also adding these same permissions to the services object that used to log in to Google?

Do you need to add the bearer token in your POST header?

https://developers.google.com/gmail/markup/actions/verifying-bearer-tokens

Something like

const dataObject = {
  message: {
    raw: CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse('Hello, World!'))
  },
  headers: {
    Authorization: `Bearer ${user.profile.services.google.accessToken}`
  }
}
2 Likes

@robfallows, you’re right! Went through the oiriginal post a bit too fast :slight_smile:

@hayk, you shouldn’t have to communicate with GoogleApi directly. Use this amazing package by @danopia https://github.com/danopia/meteor-google-api/ which takes care of the handshakes for you.

:smile: - that’s what I usually do, and miss the point entirely. I’m so happy it was your turn today :wink:

2 Likes

@robfallows It seems to work this way. Now I get an error like this

code: 400,
I20161220-11:21:45.943(4)?         message: 'Media type \'application/octet-stream\' is not supported. Valid media types: [message/rfc822]' } } }

But this means that the auth was a success and the error is now about the message.raw isn’t it?

@ramez As I stated in the op I also tried using that package but I was getting the same error. Do you have any idea why?

Correct.

I haven’t used the gmail API, but the docs for creating a draft seem to show that message is a property of resource:

/**
 * Create Draft email.
 *
 * @param  {String} userId User's email address. The special value 'me'
 * can be used to indicate the authenticated user.
 * @param  {String} email RFC 5322 formatted String.
 * @param  {Function} callback Function to call when the request is complete.
 */
function createDraft(userId, email, callback) {
  // Using the js-base64 library for encoding:
  // https://www.npmjs.com/package/js-base64
  var base64EncodedEmail = Base64.encodeURI(email);
  var request = gapi.client.gmail.users.drafts.create({
    'userId': userId,
    'resource': {
      'message': {
        'raw': base64EncodedEmail
      }
    }
  });
  request.execute(callback);
}

This is how we handle GoogleApi calls in our app – note that this is not the perculate one but the one by @danopia as it adds support for custom hosts.

self.HOST = "https://<YOURHOST>.googleapis.com";

self.callApi = function(url,params,callback,type) {
      var options = { host: self.HOST };
      if (params) options.params = params;
      if (type == undefined || type == null || type == 'get') {
        GoogleApi.get(url, options, callback || self.DEBUG_CALLBACK);
      }
      else {
        /// handle post calls which we do not use in our app
      }
 }

 self.DEBUG_CALLBACK = function() {
      return console.log(arguments);
 }