"Bad Request" error for chromeosdevices.list - google-apps-script

I am trying to figure out how to get device information about Chromebooks that my organization uses (using Google Apps Scripts.) Being new to this, I basically tried to modify a tutorial code that would get users from the organization. When I execute the code for users, it works fine, but gives me a "Bad Request (line 7, file "Code") error when I try to run it as below:
function getDevices() {
var optionalArgs = {
customerId: 'XXXXXXXXXXX',
maxResults: 10,
orderBy: 'serialNumber'
};
var response = AdminDirectory.Chromeosdevices.list(optionalArgs);
var devices = response.devices;
if (devices && devices.length > 0) {
Logger.log('Devices:');
for (i = 0; i < devices.length; i++) {
var device = devices[i];
Logger.log('%s (%s)', device.serialNumber, device.lastSync);
}
} else {
Logger.log('No devices found.');
}
}
I know it's referencing this line for the error:
var response = AdminDirectory.Chromeosdevices.list(optionalArgs);
I checked with Google's Directory API documentation to make sure I was passing parameters correctly, but I don't see that being an issue. I have enabled the API under Advanced Google Services, and enabled it on the Cloud Platform API Dashboard (the dashboard shows the requests and errors of my attempts.)
Any ideas?

I tinkered around with the script and tried entering the customerID directly to the parameters instead of passing it with the other arguments, and I no longer receive the error:
function getDevices() {
var optionalArgs = {
maxResults: 200,
orderBy: 'serialNumber'
};
var response = (AdminDirectory.Chromeosdevices.list("my_customer", optionalArgs));
I realize the that my_customer parameter is optional with the Users.list request, it is required with Chromeosdevices.list. I am not sure why it does not like having that argument passed, it seems to have been the root of my problem. While it didn't list any devices for me in the log, it did post "No devices found", which means the everything else executed.

Related

How do I display PayPal's IPN variables to a blogger.com webpage?

I've written the following PayPal IPN Listener as a stand alone webapp in Google Apps Script; executed as me everyone has access. See Fig 1 and Embedded it with within one of the pages on my blogger site - https://???.blogspot.com/p/MyPage.html. (After checkout in PayPal is completed the customer is automatically redirected to this page) See Fig 2 for embed code.
The problem is that whenever I run the script I keep getting the following error:
Script function not found: doGet
Fig 1.
function doPost(e) {
var isProduction = false;
var strSimulator = "https://ipnpb.sandbox.paypal.com/cgi-bin/webscr";
var strLive = "https://ipnpb.paypal.com/cgi-bin/webscr";
var paypalURL = strSimulator;
if (isProduction) paypalURL = strLive;
var payload = "cmd=_notify-validate&" + e.postData.contents;
var options =
{
"method" : "post",
"payload" : payload,
};
var resp = UrlFetchApp.fetch(paypalURL, options); //Handshake with PayPal - send acknowledgement and get VERIFIED or INVALID response
if (resp == 'VERIFIED') {
if (e.parameter.payment_status == 'Completed') {
if (e.parameter.receiver_email == 'my-email#example.com') {
//Implement paid amount validation. If accepting payments in multiple currencies, use e.parameter.exchange_rate to convert to reference currency (USD) if paid in any other currency
if (amountValid) {
//All validated - can process the payment here
var params = JSON.stringify(e);
return HtmlService.createHtmlOutput(' ').setSandboxMode(HtmlService.SandboxMode.IFRAME).setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
if (!(processSuccess)) {
//Process of payment failed - raise notification to check it out
}
} else {
//Payment does not equal expected purchase value
}
} else {
//Request did not originate from my PayPal account
}
} else {
//Payment status not Completed
}
} else
{
//PayPal response INVALID
}
}
Fig 2.
<iframe frameborder="0" height="500" marginheight="0" marginwidth="0" src="https://script.google.com/macros/s/My_Script_Id/exec?embedded=true" width="Auto">Loading...</iframe>
I tried calling the doPost(e) function from a doGet(e) function I created within the file; see Fig 3, but I only get the following error:
TypeError: Cannot read properties of undefined (reading 'contents') (line 15, file "Code")
Fig 3.
function doGet(e) {
doPost(e);
}
What should happen is, I would like to print out the PayPal IPN variables sent.

Supply API key to avoid Hit Limit error from Maps Service in Apps Script

I have a Google Sheet where we are fetching the driving distance between two Lat/Lng via the Maps Service. The function below works, but the matrix is 4,500 cells, so I'm getting the "Hit Limit" error.
How can I supply my paid account's API key here?
Custom Function
function drivingMeters(origin, destination) {
if (origin=='' || destination==''){return ''}
var directions = Maps.newDirectionFinder()
.setOrigin(origin)
.setDestination(destination)
.getDirections();
return directions.routes[0].legs[0].distance.value ;
}
Example use:
A1: =drivingMeters($E10,G$9)
Where E10 = 42.771328,-91.902281
and G9 = 42.490390,-91.1626620
Per documentation, you should initialize the Maps service with your authentication details prior to calling other methods:
Your client ID and signing key can be obtained from the Google Enterprise Support Portal. Set these values to null to go back to using the default quota allowances.
I recommend storing these values in PropertiesService and using CacheService, to provide fast access. Using this approach, rather than writing them in the body of your script project, means they will not be inadvertently copied by other editors, pushed to a shared code repository, or visible to other developers if your script is published as a library.
Furthermore, I recommend rewriting your custom function to accept array inputs and return the appropriate array output - this will help speed up its execution. Google provides an example of this on the custom function page: https://developers.google.com/apps-script/guides/sheets/functions#optimization
Example with use of props/cache:
function authenticateMaps_() {
// Try to get values from cache:
const cache = CacheService.getScriptCache();
var props = cache.getAll(['mapsClientId', 'mapsSigningKey']);
// If it wasn't there, read it from PropertiesService.
if (!props || !props.mapsClientId || !props.mapsSigningKey) {
const allProps = PropertiesService.getScriptProperties().getProperties();
props = {
'mapsClientId': allProps.mapsClientId,
'mapsSigningKey': allProps.mapsSigningKey
};
// Cache these values for faster access (max 6hrs)
cache.putAll(props, 21600);
}
// Apply these keys to the Maps Service. If they don't exist, this is the
// same as being a default user (i.e. no paid quota).
Maps.setAuthentication(props.mapsClientId, props.mapsSigningKey);
}
function deauthMaps_() {
Maps.setAuthentication(null, null);
}
// Your called custom function. First tries without authentication,
// and then if an error occurs, assumes it was a quota limit error
// and retries. Other errors do exist (like no directions, etc)...
function DRIVINGMETERS(origin, dest) {
if (!origin || !destination)
return;
try {
return drivingMeters_(origin, dest);
} catch (e) {
console.error({
message: "Error when computing directions: " + e.message,
error: e
});
// One of the possible errors is a quota limit, so authenticate and retry:
// (Business code should handle other errors instead of simply assuming this :) )
authenticateMaps_();
var result = drivingMeters_(origin, dest);
deauthMaps_();
return result;
}
}
// Your implementation function.
function drivingMeters_(origin, dest) {
var directions = Maps.newDirectionFinder()
...
}

Check if user has run it

I run a Google Apps script that uploads a file to the user's Google Drive file:
function doGet(e) {
var blob = UrlFetchApp.fetch(e.parameters.url).getBlob();
DriveApp.createFile(blob);
return HtmlService.createHtmlOutput("DONE!");
}
My site loads a popup window that runs a Google Apps Script with that code. Works fine.
Now, how do I communicate back to my site that they user has successfully uploaded the file? As in, how can I communicate back to my server that the user has run doGet()?`
Some type of response handling must exist?
Full working code (test it out on JSBin):
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
</head>
<body>
<div class="google-upload" data-url="https://calibre-ebook.com/downloads/demos/demo.docx">
<span style="background-color: #ddd">Upload</span>
</div>
<script>
$(function() {
$(".google-upload").click(function() {
var url = "https://script.google.com/macros/s/AKfycbwsuIcO5R86Xgv4E1k1ZtgtfKaENaKq2ZfsLGWZ4aqR0d9WBYc/exec"; // Please input the URL here.
var withQuery = url + "?url=";
window.open(withQuery + $('.google-upload').attr("data-url"), "_blank", "width=600,height=600,scrollbars=1");
});
});
</script>
</body>
</html>
So to clarify, I want a way to find out whether if the user has successfully uploaded the file. Something like:
request.execute(function(response) {
if (response.code == 'uploaded') {
// uploaded, do stuff
} else {
// you get the idea...
}
});
Adding a bounty for a complete solution to this.
Rather than returning a HtmlService object, you can pass data using jQuery's $.getJSON method and retrieve data from the doGet function with ContentService. Google Apps Script does not accept CORS, so using JSONP is the best way to get data to and from your script. See this post for more.
Working CodePen Example
I split your HTML and scripts for clarity. None of the HTML changed from your original example.
Code.gs
function doGet(e) {
var returnValue;
// Set the callback param. See https://stackoverflow.com/questions/29525860/
var callback = e.parameter.callback;
// Get the file and create it in Drive
try {
var blob = UrlFetchApp.fetch(e.parameters.url).getBlob();
DriveApp.createFile(blob);
// If successful, return okay
// Structure this JSON however you want. Parsing happens on the client side.
returnValue = {status: 'okay'};
} catch(e) {
Logger.log(e);
// If a failure, return error message to the client
returnValue = {status: e.message}
}
// Returning as JSONP allows for crossorigin requests
return ContentService.createTextOutput(callback +'(' + JSON.stringify(returnValue) + ')').setMimeType(ContentService.MimeType.JAVASCRIPT);
}
Client JS
$(function() {
$(".google-upload").click(function() {
var appUrl = "https://script.google.com/macros/s/AKfycbyUvgKdhubzlpYmO3Marv7iFOZwJNJZaZrFTXCksxtl2kqW7vg/exec";
var query = appUrl + "?url=";
var popupUrl = query + $('.google-upload').attr("data-url") + "&callback=?";
console.log(popupUrl)
// Open this to start authentication.
// If already authenticated, the window will close on its own.
var popup = window.open(popupUrl, "_blank", "width=600,height=600,scrollbars=1");
$.getJSON(popupUrl, function(returnValue) {
// Log the value from the script
console.log(returnValue.status);
if(returnValue.status == "okay") {
// Do stuff, like notify the user, close the window
popup.close();
$("#result").html("Document successfully uploaded");
} else {
$("#result").html(returnValue);
}
})
});
});
You can test the error message by passing an empty string in the data-url param. The message is returned in the console as well as the page for the user.
Edit 3.7.18
The above solution has problems with controlling the authorization flow. After researching and speaking with a Drive engineer (see thread here) I've reworked this into a self-hosted example based on the Apps Script API and running the project as an API executable rather than an Apps Script Web App. This will allow you to access the [run](https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run) method outside an Apps Script web app.
Setup
Follow the Google Apps Script API instructions for JavaScript. The Apps Script project should be a standalone (not linked to a document) and published as API executable. You'll need to open the Cloud Console and create OAuth credentials and an API key.
The instructions have you use a Python server on your computer. I use the Node JS server, http-server, but you can also put it live online and test from there. You'll need to whitelist your source in the Cloud Console.
The client
Since this is self hosted, you'll need a plain HTML page which authorizes the user through the OAuth2 API via JavaScript. This is preferrable because it keeps the user signed in, allowing for multiple API calls to your script without reauthorization. The code below works for this application and uses the authorization flow from the Google quickstart guides.
index.html
<body>
<!--Add buttons to initiate auth sequence and sign out-->
<button id="authorize-button" style="display: none;">Authorize</button>
<button id="signout-button" style="display: none;">Sign Out</button>
<button onclick="uploadDoc()" style="margin: 10px;" id="google-upload" data-url="https://calibre-ebook.com/downloads/demos/demo.docx">Upload doc</button>
<pre id="content"></pre>
</body>
index.js
// Client ID and API key from the Developer Console
var CLIENT_ID = 'YOUR_CLIENT_ID';
var API_KEY = 'YOUR_API_KEY';
var SCRIPT_ID = 'YOUR_SCRIPT_ID';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://script.googleapis.com/$discovery/rest?version=v1"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/script.external_request';
var authorizeButton = document.getElementById('authorize-button');
var signoutButton = document.getElementById('signout-button');
var uploadButton = document.getElementById('google-upload');
var docUrl = uploadButton.getAttribute('data-url').value;
// Set the global variable for user authentication
var isAuth = false;
/**
* On load, called to load the auth2 library and API client library.
*/
function handleClientLoad() {
gapi.load('client:auth2', initClient);
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick;
signoutButton.onclick = handleSignoutClick;
// uploadButton.onclick = uploadDoc;
});
}
/**
* Called when the Upload button is clicked. Reset the
* global variable to `true` and upload the document.
* Thanks to #JackBrown for the logic.
*/
function updateSigninStatus(isSignedIn) {
if (isSignedIn && !isAuth) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
uploadButton.style.display = 'block'
uploadButton.onclick = uploadDoc;
} else if (isSignedIn && isAuth) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
uploadButton.style.display = 'block';
uploadDoc();
} else {
authorizeButton.style.display = 'block';
signoutButton.style.display = 'none';
uploadButton.style.display = 'none';
isAuth = false;
}
}
/**
* Sign in the user upon button click.
*/
function handleAuthClick(event) {
gapi.auth2.getAuthInstance().signIn();
isAuth = true; // Update the global variable
}
/**
* Sign out the user upon button click.
*/
function handleSignoutClick(event) {
gapi.auth2.getAuthInstance().signOut();
isAuth = false; // update the global variable
}
/**
* Append a pre element to the body containing the given message
* as its text node. Used to display the results of the API call.
*
* #param {string} message Text to be placed in pre element.
*/
function appendPre(message) {
var pre = document.getElementById('content');
var textContent = document.createTextNode(message + '\n');
pre.appendChild(textContent);
}
/**
* Handle the login if signed out, return a Promise
* to call the upload Docs function after signin.
**/
function uploadDoc() {
console.log("clicked!")
var docUrl = document.getElementById('google-upload').getAttribute('data-url');
gapi.client.script.scripts.run({
'scriptId':SCRIPT_ID,
'function':'uploadDoc',
'parameters': [ docUrl ]
}).then(function(resp) {
var result = resp.result;
if(result.error && result.error.status) {
// Error before the script was Called
appendPre('Error calling API');
appendPre(JSON.parse(result, null, 2));
} else if(result.error) {
// The API executed, but the script returned an error.
// Extract the first (and only) set of error details.
// The values of this object are the script's 'errorMessage' and
// 'errorType', and an array of stack trace elements.
var error = result.error.details[0];
appendPre('Script error message: ' + error.errorMessage);
if (error.scriptStackTraceElements) {
// There may not be a stacktrace if the script didn't start
// executing.
appendPre('Script error stacktrace:');
for (var i = 0; i < error.scriptStackTraceElements.length; i++) {
var trace = error.scriptStackTraceElements[i];
appendPre('\t' + trace.function + ':' + trace.lineNumber);
}
}
} else {
// The structure of the result will depend upon what the Apps
// Script function returns. Here, the function returns an Apps
// Script Object with String keys and values, and so the result
// is treated as a JavaScript object (folderSet).
console.log(resp.result)
var msg = resp.result.response.result;
appendPre(msg);
// do more stuff with the response code
}
})
}
Apps Script
The Apps Script code does not need to be modified much. Instead of returning using ContentService, we can return plain JSON objects to be used by the client.
function uploadDoc(e) {
Logger.log(e);
var returnValue = {};
// Set the callback URL. See https://stackoverflow.com/questions/29525860/
Logger.log("Uploading the document...");
try {
// Get the file and create it in Drive
var blob = UrlFetchApp.fetch(e).getBlob();
DriveApp.createFile(blob);
// If successful, return okay
var msg = "The document was successfully uploaded!";
return msg;
} catch(e) {
Logger.log(e);
// If a failure, return error message to the client
return e.message
}
}
I had a hard time getting CodePen whitelisted, so I have an example hosted securely on my own site using the code above. Feel free to inspect the source and take a look at the live Apps Script project.
Note that the user will need to reauthorize as you add or change scopes in your Apps Script project.

Google Drive API push notifications can't be stopped / cancelled

I am watching a Drive resource. Setting up the watch (using the googleapis 0.2.13-alpha client with node.js and drive.file.watch):
exports.subscribeDriveCallbacksCmd = function( user, fileId ){
var userId = user.id;
var baseUrl = exports.BASE_URL;
var subscribeUrl = baseUrl+"/incoming/file";
var watchId = 'id-'+fileId+'-'+(new Date()).getTime();
var subscription = {
id: watchId,
token: userId+':'+fileId,
type: 'web_hook',
address: subscribeUrl,
params:{
ttl: 600
}
};
var params = {
fileId: fileId
};
//var cmd = client.drive.files.watch( params, subscription );
// FIXME - Hack around bug in RPC implememntation
var hack = {channel:subscription};
for( var key in params ){
hack[key] = params[key];
}
var cmd = client.drive.files.watch( hack );
return cmd;
};
var cmd = exports.subscribeDriveCallbacksCmd( user, '0ZZuoVaqdWGhpUk9PZZ' );
var batch = client.newBatchRequest();
batch.add(cmd);
batch.withAuthClient(user.auth).execute(cb);
After this, I'm getting a response of
{ kind: 'api#channel',
id: 'id-0ZZuoVaqdWGhpUk9PZZ-1374536746592',
resourceId: 'WT6g4bx-4or2kPWsL53z7YxZZZZ',
resourceUri: 'https://www.googleapis.com/drive/v2/files/0AHuoVaqdWGhpUkZZZZ?updateViewedDate=false&alt=json',
token: '101852559274654726533:0ZZuoVaqdWGhpUk9PZZ',
expiration: '1374537347934' }
and a sync callback with the following headers
'x-goog-channel-id': 'id-0ZZuoVaqdWGhpUk9PZZ-1374536746592',
'x-goog-channel-expiration': 'Mon, 22 Jul 2013 23:55:47 GMT',
'x-goog-resource-state': 'sync',
'x-goog-message-number': '1',
'x-goog-resource-id': 'WT6g4bx-4or2kPWsL53z7YxZZZZ',
'x-goog-resource-uri': 'https://www.googleapis.com/drive/v2/files/0AHuoVaqdWGhpUkZZZZ?updateViewedDate=false&alt=json',
'x-goog-channel-token': '101852559274654726533:0ZZuoVaqdWGhpUk9PZZ',
'user-agent': 'APIs-Google; (+http://code.google.com/apis)
There are several problems with this, however:
The resource-id returned by both of these do not match the fileId passed when I subscribed to the watch. It does match the ID given in the resource-uri
Trying to use either the resourceID returned here, or the fileId passed when I subscribed, returns an error when I try to stop the channel.
The error given for drive.channel.stop varies depending on how I do the call. If I use the API Explorer at the bottom of the Channel: Stop page, providing either the resourceId or the fileId for the resourceId parameter, I get
404 Not Found
{
"error": {
"errors": [
{
"domain": "global",
"reason": "notFound",
"message": "Channel not found"
}
],
"code": 404,
"message": "Channel not found"
}
}
If I use the node.js library with this code:
exports.cancelDriveCallbacksCmd = function( watchId, fileId, resourceId ){
var body = {
id: watchId,
resourceId: resourceId
};
var cmd = client.drive.channels.stop( body );
return cmd;
};
var cmd = exports.cancelDriveCallbacksCmd( 'id-0ZZuoVaqdWGhpUk9PZZ-1374536746592', '0ZZuoVaqdWGhpUk9PZZ', 'WT6g4bx-4or2kPWsL53z7YxZZZZ' );
var batch = client.newBatchRequest();
batch.add(cmd);
batch.withAuthClient(user.auth).execute(cb);
I get the error
{ code: 500,
message: 'Internal Error',
data:
[ { domain: 'global',
reason: 'internalError',
message: 'Internal Error' } ] }
which I suspected was related to Bug 59 which has a workaround (which was the hack code I was using above) but should have the fix in place sometime this week, I understand.
So I changed it to this code, which worked around the bug for files.watch:
exports.cancelDriveCallbacksCmd = function( watchId, fileId, resourceId ){
var params = {};
var body = {
id: watchId,
resourceId: resourceId,
fileId: fileId
};
//var cmd = client.drive.channels.stop( params, body );
// FIXME - hack around bug in RPC implementation
var hack = {channel:body};
for( var key in params ){
hack[key] = params[key];
}
var cmd = client.drive.channels.stop( hack );
console.log( 'cancelDriveCallbacksCmd', hack );
return cmd;
};
But I get the same 500 error.
Any thoughts about what I might be doing wrong or how to even go about debugging where I might be going wrong with it?
Push notification is designed to watch any api resource, although it only supports Changes and Files for now. Thus, it needs unique resourceId for all resource type. That is the reason why they have resourceId that is not equal to fileId.
Confirmations do come back with info about which file it is watching. Check header of your response. Also, you can make use of token to save channel specific information if you want.
If you are using API explorer, you cannot unsubscribe from the channel because as you know, push notification requires additional verification of url through apis console and apis explorer is not authenticated to access your notification. It is working as intended by security reason. I will report about this issue to stop people from getting confused with this.
fileId doesn't go to request body. It should be one of the parameters. Also, you should make request to Channels.stop() to unsubscribe. Something like this:
Code to subscribe:
var channel= {
'id': 'yourchannelid',
'type': 'web_hook',
'address': 'https://notification.endpoint'
};
var request = client.drive.files.watch({'fileId': fileId, 'channel':channel});
Code to unsubscribe
var request = client.drive.channels.stop({'id': channelId, 'resourceId':resourceId});

GMailApp rateMax - service invoked too fast

I published a Google Apps Script as a WebApp, but sometimes the users using the script run into:
Service invoked too many times in a short time: gmail rateMax. Try Utilities.sleep(1000) between calls. (line XXX)
The exception tells me to slow it down with a Utitlities.sleep(1000), however before doing that I'd like to know what exactly the maximum rate is. The only documentation I can find about this is the quota page, but this says:
GMail Read: 10000 / day
and my script is far away from 10000 reads.
Does anybody know what rateMax exactly refers to?
Update: The code causing this is the following (gets called via XHR):
add = function(form) {
// [...]
messageId = (_ref = form.msgId) != null ? _ref : form.messageId;
if (!messageId || !(message = GmailApp.getMessageById(messageId))) {
throw ErrorCodes.INVALID_MESSAGE_ID;
}
// [...]
thread = GmailApp.getThreadById(message.getThread().getId());
if (String(form.archive) === "true") {
thread.moveToArchive();
}
// [...]
addLabel(LABEL_BASE, thread);
addLabel(LABEL_OUTBOX, thread);
};
getLabel = function(name, create) {
var _ref;
return (_ref = GmailApp.getUserLabelByName(name)) != null ? _ref : (create ? GmailApp.createLabel(name) : void 0);
};
addLabel = function(name, thread) {
var _ref;
if ((_ref = this.getLabel(name, true)) != null) {
_ref.addToThread(thread);
}
};
// [...] denotes removed code from the sample that does not do calls to the GMail API.
Were you checking attachments, or doing something else? There was a bug that should be fixed now that triggered this when loading attachments.