How can I call to Smartsheet API using Google Apps script? - google-apps-script

I have used Postman and Charles to see if my Smartsheet GET function works, and all is well, I get the data json string back.
I have tried running the call from local code and from a Google app script html page.
But I get this error from the Google app script page:
"XMLHttpRequest cannot load https://api.smartsheet.com/2.0/sheets/ MY SMART SHEET ID. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://n-n662xy6uqbadudjpoghatx4igmurid667k365ni-script.googleusercontent.com' is therefore not allowed access."
It is my aim to update a Google sheet automatically from a Smartsheet sheet.
My Ajax request looks like this:
var settings = {
"async": true,
"crossDomain": true,
"url": "https://api.smartsheet.com/2.0/sheets/SHEET_ID",
"method": "GET",
"headers": {
"authorization": "Bearer MY_SECRET_ACCESS_TOKEN",
"cache-control": "no-cache",
"postman-token": "SOME_LONG_TOKEN"
}
}
$.ajax(settings).done(function (response) {
console.log(response);
});

You cannot call the Smartsheet API from client-side JavaScript due to the fact that the API doesn't support CORS at this time.
You can call the Smartsheet API directly from a Google Apps Script. In fact, we/Smartsheet publish two Google Add-ons that both use the Smartsheet API from scripts (1,2).
The Google apps-script-oauth2 project provides a complete example of using the Smartsheet API in their sample directory on GitHub. See samples/Smartsheet.gs.
With the OAuth token out of the way, you can make requests to the Smartsheet API like so:
var url = 'https://api.smartsheet.com/2.0/users/me';
var options = {
'method': 'get'
, 'headers': {"Authorization": "Bearer " + getSmartsheetService().getAccessToken() }
};
var response = UrlFetchApp.fetch(url, options).getContentText();
Logger.log("email:" + JSON.parse(response).email);
Note that getSmartsheetService() in the above example is just like getDriveService() in Google's Readme except for Smartsheet. The full code is below:
function getSmartsheetService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth2.createService('scott_smartsheet')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://app.smartsheet.com/b/authorize')
.setTokenUrl('https://api.smartsheet.com/2.0/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId(SMARTSHEET_CLIENT_ID)
.setClientSecret(SMARTSHEET_CLIENT_SECRET)
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
// Set the scopes to request (space-separated for Google services).
.setScope('READ_SHEETS')
// Set the handler for adding Smartsheet's required SHA hash parameter to the payload:
.setTokenPayloadHandler(smartsheetTokenHandler)
;
}

Under external APIs under Google Apps Script API,
Google Apps Script can interact with APIs from all over the web.
Connecting to public APIs
Dozens of Google APIs are available in Apps Script, either as built-in services or advanced services. If you want to use a Google (or non-Google) API that isn't available as an Apps Script service, you can connect to the API's public HTTP interface through the URL Fetch service.
The following example makes a request to the YouTube API and returns a feed of videos that match the query skateboarding dog.
var url = 'https://gdata.youtube.com/feeds/api/videos?'
+ 'q=skateboarding+dog'
+ '&start-index=21'
+ '&max-results=10'
+ '&v=2';
var response = UrlFetchApp.fetch(url);
Logger.log(response);
Here is a related SO ticket that connected his code in google apps script to smartsheet api.

Related

How do you call Square's APIs to Google Sheets using Google Apps Script?

Hello! I'd appreciate any help in making a connection. Here's what I'm trying to 'Get' info from:
curl https://connect.squareup.com/v2/locations
-H 'Square-Version: 2022-01-20'
-H 'Authorization: Bearer ACCESS_TOKEN'
-H 'Content-Type: application/json'
Setting the Scopes:
First I set the following scopes in the manifest file (here's a picture). I followed a similar notation as google's.
The Apps Script:
function squareLocations() {
var url = "https://connect.squareup.com/v2/locations";
var headers = {
"contentType":"application/json",
"headers": {"Square-Version": "2022-01-20",
"Authorization": "Bearer <TOKEN>"}
};
var data =
{
'locations':
{
'id': locationID,
'name': locationName,
'address': locationAdress,
}
}
var response = UrlFetchApp.fetch(url, headers);
var text = response.getResponseCode();
var json = JSON.parse(response.getContextText());
Logger.log (text);
Logger.log (json);
}
response returns a 400 error: invalid scope (lists all scopes).
In the appsscript.json file remove the OAuth scope from square. On this file only include the OAuth scopes required by the Google Apps Script and Advanced Services (like Advanced Sheets Service).
The scopes of APIs called using by UrlFetchApp.fetch() might be included when generating the token or on the corresponding API console / App settings (I'm sorry , I don't know the details of Square API).
Background: why the manifest scopes are not relevant to the Square API##
The Square API uses the Oauth2 protocol. The idea of this protocol is as follows: you provide the user an opportunity to log in to their Square account, and in the process, you capture an oauth token that represents that user's login. This token allows your script to take action on the user's behalf. The scopes you specify tell the user what types of actions you'll be performing on their behalf, and you are limited to only those actions when calling the API.
The scopes listed in the Apps Script manifest represent the same idea, but for Google's services, not any external services like Square. When you call ScriptApp.getOauthToken(), you get a token for performing actions within the Google account of the user currently running the Apps Script script. (The reason you can do this for Google services without presenting a login screen is that the Google user has already logged in in order to run the script in the first place.)
But for any non-Google API like Square, you need to set up a full OAuth2 process, as detailed below:
The main question: How to access Square's API from Google Apps Script
There is an OAuth2 library for Apps Script that handles most of the mechanics of obtaining and storing the token for any API that uses OAuth2. Start by adding the library to your script (the library's script id is 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF).
Then obtain the callback uri for your project. Do this by executing the following function from the script editor:
function logRedirectUri()
{
console.log(
OAuth2.getRedirectUri()
)
}
Now go to the Square developer dashboard, and create a new application. There's an OAuth link on the left side of the app's main screen. Go to that link and you'll get the app's Application ID and Application Secret. You'll need those for your script. In addition, on this screen you should add the redirect uri you obtained above (when the user logs in to Square, the Square API will now know to redirect to this uri, which your script uses to record the oauth token).
Now you're ready to use the OAuth library to provide the Square sign-in process and then call the API. I've created a repo of the code I use for doing this, which you should be able to drop in to your script, but the relevant points are:
In the getSquareService function, set SQUARE_CLIENT_ID and SQUARE_CLIENT_SECRET to the id and secret you got from the Square developer dashboard.
getSquareService is also where you list the scopes you want for the Square API.
function getSquareService()
{
var SQUARE_CLIENT_SECRET = '{your square application secret}'
var SQUARE_CLIENT_ID = '{your square application id}'
return OAuth2.createService('Square')
// Set the endpoint URLs.
.setAuthorizationBaseUrl('https://connect.squareup.com/oauth2/authorize')
.setTokenUrl('https://connect.squareup.com/oauth2/token')
// Set the client ID and secret.
.setClientId(SQUARE_CLIENT_ID)
.setClientSecret(SQUARE_CLIENT_SECRET)
// Set the name of the callback function that should be invoked to
// complete the OAuth flow.
.setCallbackFunction('authCallbackSquare')
// Set the property store where authorized tokens should be persisted.
// Change this to .getUserProperties() if you are having multiple google users authorize the service:
// this will prevent one user's token from being visible to others.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scopes needed. For a full list see https://developer.squareup.com/docs/oauth-api/square-permissions
.setScope(
[
'ORDERS_WRITE',
'PAYMENTS_WRITE',
'PAYMENTS_READ',
'ORDERS_READ',
'MERCHANT_PROFILE_READ'
]
.join(" ")
)
// Set grant type
.setGrantType('authorization_code')
}
And include the callback function that will store the token:
function authCallbackSquare(request)
{
var service = getSquareService();
// Now process request
var authorized = service.handleCallback(request);
if (authorized)
{
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else
{
return HtmlService.createHtmlOutput('Denied. You can close this tab.');
}
}
If your script is bound to a spreadsheet, you can run authorizeSquareUser() in the repo to get a sidebar that initiates the authorization flow. Otherwise, run this modified version:
function authorizeSquareUser()
{
var service = getSquareService();
var authorizationUrl = service.getAuthorizationUrl();
console.log("Auth URL is %s", authorizationUrl);
}
and then visit the url that is logged. At this url you will log in to your square account, and then you should be redirected to a page that says "Success! You can close this tab."
At this point, the Square user's OAuth token has been stored in the script properties of your project. You are now ready to make a call to the Square API. When doing this, you access the stored token using getSquareService().getAccessToken(), so your headers will look like
var headers = {
'Authorization': 'Bearer ' + getSquareService().getAccessToken(),
'Square-Version': '2022-01-20',
'Content-Type': 'application/json'
}
Then you can call
UrlFetchApp.fetch(url, {'method': 'GET', 'headers': headers}) // Change GET method depending on the action you are performing

How to Properly Configure GAS Web App (as another user) to Execute GAS API Executable (as me) using OAuth2?

Problem
After days of reading and attempting trial-and-error, I am trying to make a call from a GAS Web App (executed as any Google User) to a GAS API Executable (executed as Me), but consistently receive an error message after Reviewing/Granting permissions:
*"Error: Access not granted or expired."*
That is not a response from the server, but simply a notification from the OAuth2 library: "This method will throw an error if the user's access was not granted or has expired."
So it seems there may be some otherwise obvious step that is missing from instructions and Q&As. Somehow, after doing all of the setup, the web app cannot access the API Executable.
I also spent a few hours writing this question and formulating a minimal test/example. Here are the files in Google Drive, for viewing directly.
Desired Outcome
The desired outcome is to be able to have other users use the Web App as themselves and, from that app, execute the API Executable as Me.
Question
What is wrong with my configuration and/or code, and how can I receive data back from the API Executable?
What I've tried
I've combined various tutorials and Q&As and attempted to make a minimal example. The three most closely related are:
Google Groups - "Webapp execute as user BUT save data to spreadsheet"
...Service accounts, while applicable, are not the best fit for this use-case. They are better suited to situations where the service account acts as a proxy for multiple users...
...Basically, you'll need to generate OAuth2 credentials specific to
your account and use the OAuth2 library to generate access tokens.
That access token can then be used to make direct calls against the
Spreadsheet REST API OR alternatively, the Apps Script API (formerly
the Execution API) to invoke a function in the script under your own
authority...
SO - "Can I have part of Google Apps Script code execute as me while the rest executes as the accessing user?"
SO - "Get user info when someone runs GAS web app as me"
The first link seems directly applicable to my scenario. However, the instructions are sparse, though I have done my best to follow them. The second is also applicable, but sparse. The third is related, but is actually the inverse of what I want to do, and so of limited help.
Summary of Steps in GCP
Here is what I did within console.cloud.google.com:
Created a new project named "apiExecTest".
Within "APIs & Services", enabled two APIs:
Apps Script API (unsure if necessary)
Google Sheets API (unsure if necessary)
Within "APIs & Services", configured the Oauth Consent Screen
Internal
Set App name, User support email, and Developer contact email. Did nothing else. Did not set "App domain" nor "Authorized domains".
Added all 61 scopes for Apps Script and Sheets (unsure if necessary)
Within "APIs & Services", created credentials
OAuth client ID
Web Application
Added Client name.
Added Authorized Redirect URI:
https://script.google.com/macros/d/1zj4ovqMWoCUgBxJJ8u518TOEKlckeIazVBL4ASdYFiVmjoZz9BLXbJ7y/usercallback
Obtained Client ID & Client Secret to insert into webApp code.
Summary of Steps in GAS
Here is what I did in Google Drive / Apps Script. The files can be viewed here:
Created a new folder in Google Drive containing three things:
GAS file: "webApp"
Deployed as Web App
Execute as: User accessing the web app
Who has access: Anyone with Google account
GAS file: "apiExec"
Deployed as API Executable
Who has access: Anyone with Google account
Google Sheet: sSheet
Not shared with anyone, owned by Me.
Added a basic function to apiExec that obtains the first cell of the first sheet in sSheet, and confirmed it works by executing it within the GAS editor and observing the console output.
Added the OAuth2 library to webApp as oauth2.gs, copy/pasted from GitHub. Setup and configured setClientId(), setClientSecret(), API URL and other settings per the readme and examples cited above. For setScope(), I used:.setScope('https://www.googleapis.com/auth/script.external_request https://www.googleapis.com/auth/spreadsheets')
Added a basic functionality to webApp that makes a call to apiExec to obtain data from sSheet.
Added the following to the webApp appsscript.json (unsure if correct, have tried variations):"oauthScopes": ["https://www.googleapis.com/auth/script.external_request", "https://www.googleapis.com/auth/spreadsheets"]
I changed the GCP Project Number for both apiExec and webApp to that of the GCP project created in the steps above.
I then executed the doGet() function of webApp within the GAS editor. It does ask for authorization, which I granted. After authorization, as the execution continues, the error mentioned above is thrown. I also ran the function via webApp's URL, which of course also results in the error.
After attempting this multiple times, and spending days reading and with trial-and-error, I've made no progress. Any help is greatly appreciated.
To be thorough, here are the contents of the GAS files:
apiExec
appsscript.json
{
"timeZone": "America/New_York",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"executionApi": {
"access": "ANYONE"
}
}
Code.gs
function doPost() {
var spreadsheet = SpreadsheetApp.openById("1aIMv1iH6rxDwXLx-i0uYi3D783dCtlMZo6pXJGztKTY");
var sheet = spreadsheet.getSheetByName("test sheet");
var data = sheet.getRange("A1").getValues()
console.log(data)
return data;
}
webApp
appsscript.json
{
"timeZone": "America/New_York",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/spreadsheets"
],
"webapp": {
"executeAs": "USER_ACCESSING",
"access": "ANYONE"
}
}
Code.gs
function doGet(e) {
var myParam = "someParam";
console.log(myParam);
var apiExecResponse = makeRequest('doPost', [myParam]);
console.log(apiExecResponse);
var appsScriptService = getService();
if (!appsScriptService.hasAccess()) {
// This block should only run once, when I authenticate as myself to create the refresh token.
var authorizationUrl = appsScriptService.getAuthorizationUrl();
var htmlOutput = HtmlService.createHtmlOutput('Authorize.');
htmlOutput.setTitle('GAS Authentication');
return htmlOutput;
}
else {
console.log("It worked: " + myParam + " " + apiExecResponse);
htmlOutput.setTitle("The Results");
return HtmlService.createHtmlOutput("<p>It worked: " + myParam + " " + apiExecResponse + "</p>");
}
}
function getService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth2.createService('apiExecService')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId('390208108732-s7geeikfvnqd52a0fhf6e015ucam0vqk.apps.googleusercontent.com')
.setClientSecret('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')
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scopes to request (space-separated for Google services).
.setScope('https://www.googleapis.com/auth/script.external_request https://www.googleapis.com/auth/spreadsheets')
// Below are Google-specific OAuth2 parameters.
// Sets the login hint, which will prevent the account chooser screen
// from being shown to users logged in with multiple accounts.
//.setParam('login_hint', Session.getEffectiveUser().getEmail())
// Requests offline access.
.setParam('access_type', 'offline')
// Consent prompt is required to ensure a refresh token is always
// returned when requesting offline access.
.setParam('prompt', 'consent');
}
function authCallback(request) {
var apiExecService = getService();
var isAuthorized = apiExecService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
}
else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
function makeRequest(functionName, paramsArray) {
console.log("Running " + functionName + " via 'makeRequest'.");
var apiExecUrl = 'https://script.googleapis.com/v1/scripts/AKfycbzHV5_Jl2gJVv0wDVp93wE0BYfxNrOXXKjIAmOoRu3D8W6CeqSQM9JKe8pOYUK4fqM_:run';
var payload = JSON.stringify({
"function": functionName,
"parameters": paramsArray,
"devMode": false
});
var params = {
method:"POST",
headers: {
Authorization: 'Bearer ' + getService().getAccessToken()
},
payload: payload,
contentType: "application/json",
muteHttpExceptions: true
};
var result = UrlFetchApp.fetch(apiExecUrl, params);
return result;
}
OAuth2.gs
See: https://github.com/googleworkspace/apps-script-oauth2/blob/master/dist/OAuth2.gs
If I understand correctly, your current flow is as follows:
Use the OAuth2 library to do a one-time capture of the auth token for your own Google account.
Use that stored token to authenticate the request to the API Executable (when running the web app as another user).
Apps Script has a built-in method for accomplishing step 1: ScriptApp.getOAuthToken(), so I'm not sure you even need the OAuth2 library for this. (You would need that library for authorizing services other than Google.)
Possibly you can avoid using the OAuth2 library completely by doing the following:
Add this function to your web app project and run it once from the editor, i.e. under your own authorization:
function storeOauthToken() {
PropertiesService.getScriptProperties().setProperty(
'myToken',
ScriptApp.getOAuthToken()
)
}
Change the headers in the makeRequest function of your webApp project from this
headers: {
Authorization: 'Bearer ' + getService().getAccessToken()
},
to this:
headers: {
Authorization: 'Bearer ' + PropertiesService.getScriptProperties().getProperty('myToken')
},
I created a copy of your projects and was able to confirm that this technique works.
Token refresh
I assume that the token may expire like any other OAuth2 token, so you may need to set a timed trigger (again, under your own authorization) to run storeOAuthToken() periodically.

OAuth problem with Apps Script and Google Chat

I'm currently trying to make a google chat bot on Apps Script, using webhooks that supposedly don't require any authentification.
However, when I run my Apps Script function from Apps Script, it seems like I have to allow my account to "use external web apps" or something along these lines.
I developped the Apps Script as an API, so that I could call the functions from an external point, but it says that I need OAuth credentials to do so. And I know how to implement OAuth credentials, I just have no idea what scope I'm supposed to use since webhooks are supposed to work without authentification.
Here's the function:
function sendText(text) {
var url = "https://chat.googleapis.com/v1/spaces/[space]/messages?key=[webhook-key]&token=[token]";
message = { 'text': text };
var message_headers = {};
const response = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(message),
});
Logger.log(response);
}
okay I found the answer myself;
scope is : https://googleapis.com/auth/script.external_request

Programmatically connect Sheets / request access

Is there a way for Google Script to request access to a Google Sheet using the Document Key? I've tried openById() and was hoping for it to trigger a request, but nope...
I have found a way which seems to work for me, but I will caveat it with the fact that it is not documented, so neither likely to be supported or recommended, and I imagine it could probably change at any time.
It requires making OAuth2 authenticated requests with the server, so there are a number of steps involved:
Ensure Drive is an enabled scope on your script
Under File > Project Properties > Scopes, check that the following scope is listed:
https://www.googleapis.com/auth/drive
If it isn't listed then running a simple call like:
DriveApp.createFile('', '');
Should prompt you to allow access to Drive. Once done, you can remove this line from your script.
Verify that the required Drive scope is now listed.
Make a request for the file you wish access for
This sample function worked for me, but YMMV:
function requestShare(spreadsheetId) {
var request = {
requestType: 'requestAccess',
itemIds: spreadsheetId,
foreignService: 'explorer',
shareService: 'explorer',
authuser: 0
};
var url = 'https://docs.google.com/sharing/commonshare';
var params = {
method: 'POST',
payload: request,
headers: {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
},
contentType: 'application/x-www-form-urlencoded'
};
var response = UrlFetchApp.fetch(url, params);
Logger.log(response.getContentText());
}
Call the requestShare function with the ID of the spreadsheet you are wishing to request access to.
I tested this between two accounts, and received sharing request emails, which when clicked, showed the dialog with the correct requestor details in.
Hope this helps.

How to authorize and post/update Trello card from a Google docs script

I have a Google Docs Spreadsheet that I'd like to use to update referenced cards in Trello. I've had some success with oauth and pulling data via their HTTP API, but am stuck with the following:
1) it seems Trello's code.js requires a window object, which the Google Doc script doesn't provide. So, I am stuck using their HTTP API.
2) authenticating via OAuth works, but only gives me read access. I cannot update cards with the token I am able to get.
function test() {
var oauthConfig = UrlFetchApp.addOAuthService("trello");
oauthConfig.setAccessTokenUrl("https://trello.com/1/OAuthGetAccessToken");
oauthConfig.setRequestTokenUrl("https://trello.com/1/OAuthGetRequestToken");
oauthConfig.setAuthorizationUrl("https://trello.com/1/authorize?key=" + consumerKey + "&name=trello&expiration=never&response_type=token&scope=read,write");
//oauthConfig.setAuthorizationUrl("https://trello.com/1/OAuthAuthorizeToken"); <-- this only gives read access. Cannot POST
oauthConfig.setConsumerKey(consumerKey);
oauthConfig.setConsumerSecret(consumerSecret);
var url = 'https://trello.com/1/cards/yOqEgvzb/actions/comments&text=Testing...';
var postOptions = {"method" : "post",
"oAuthServiceName": "trello",
"oAuthUseToken": "always"};
var response = UrlFetchApp.fetch(url, postOptions); // "Request failed for returned code 404. Truncated server response: Cannot POST"
Logger.log(response.getContentText());
}
I've found a number of related questions but no direct answers:
How to get a permanent user token for writes using the Trello API?
Trello API: vote on a card
Trello API: How to POST a Card from Google Apps Script (GAS)
Google apps script oauth connect doesn't work with trello
Many thanks ahead of time for any advice.
In order to get write access, you need to change the authorization url.
This example works for me
var oauthConfig = UrlFetchApp.addOAuthService("trello");
oauthConfig.setAccessTokenUrl("https://trello.com/1/OAuthGetAccessToken");
oauthConfig.setRequestTokenUrl("https://trello.com/1/OAuthGetRequestToken");
oauthConfig.setAuthorizationUrl("https://trello.com/1/OAuthAuthorizeToken?scope=read,write");
on 1) yes you cant use the library from server gas, its meant to be run from a browser.
on 2), Ive done it from GAS with write access without problems. You need to use the format:
https://api.trello.com/1/.../xxxx?key=yyyyyy&token=zzzzzzz&...
and when you get the token, you need to request permanent access (no expiration) and write access, as in:
https://trello.com/1/authorize?key="+key+"&name=xxxxxxx&expiration=never&response_type=token&scope=read,write"
As in:
function postNewCardCommentWorker(cardId, comment, key, token) {
var commentEncoded=encodeURIComponent(comment);
var url = "https://api.trello.com/1/cards/"+cardId+"/actions/comments?text="+commentEncoded+"&key="+key+"&token="+token;
var options =
{
"method" : "POST"
};
UrlFetchApp.fetch(url, options);
}