I'm trying to create a bot using GoogleChatAPI.
Here is what I am trying to do:
If a User adds the bot that I'm creating, I want the bot to show a message of caution for using the bot.
If a User posts a message in the bot, the bot will post that message to another chat room.
The method of posting a message is "UrlFetchApp.fetch" (and add an OAUTH scope at the same time)
To achieve task 1, I have used the code onAddToSpace.
To achieve task 2, I have used the code onMessage.
But when the Users add the bot, onAddToSpace does not get ignited. This event isn't in the Executions List.
But when I delete the code UrlFetchApp.fetch (and delete an OAUTH scope at the same time), then onAddToSpace gets ignited.
How can I write the codes to achieve the two things I'm trying to do at the same time?
Or is it even possible?
Code:
function onMessage(event) {
var name = "";
var message = "ご依頼承りました!情シスからご連絡いたしますので少々お待ち下さい。";
postICTRoom(event);
return { "text": message };
}
function onAddToSpace(event) {
var message = "情シスへ質問、依頼ができるbotです。送信したメッセージは、ユーザー名付きですべて情シスチームチャットに送信されます。遊びが一切ないbotです、ご利用の際は十分ご注意ください。ここは万事屋ではありません。";
return { "text": message };(*1)
}
function onRemoveFromSpace(event) {
console.info("Bot removed from ",
(event.space.name ? event.space.name : "this chat"));
}
function postICTRoom(event){
var messageDate = new Date()
var url = "https://chat.googleapis.com/v1/spaces/...";
var thread = "spaces/..."
var payload = {
"text" : event.user.displayName + " " + Utilities.formatDate(messageDate, "JST", 'yyyy/MM/dd_HH:mm:ss') + "\n" + event.message.text,
"thread": {
"name": thread
}
}
var json = JSON.stringify(payload);
var options = {
'method': 'POST',
'contentType': 'application/json; charset=UTF-8',
"payload" : json
};
var response = UrlFetchApp.fetch(url, options);
Manifest:
{
"timeZone": "Asia/Tokyo",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"chat": {
}
}
OAuthScope
https://www.googleapis.com/auth/script.external_request
Update
onAddToSpace wasn't executed when I added the bot.
I posted a message and approved OAuth, then onMessage got executed.
After when I approved OAuth, and I once removed the bot, I added it again, onAddToSpace was already executed as I have expected...
I want to know if it is possible and if so, how I can get onAddToSpace executed before I have to approve OAuth.
Update2
Example of onAddToSpace successed(1)
Set scopes script.external_request and chat.
Added the bot.onAddToSpace was not executed at that time, because the log didn't display on screen and the bot didn't post a message(point of (*1) in Code).
Posted a message. onMessage successed. At that time I approved OAuth.
Removed the bot and added it again. onAddToSpace was executed the first time.
Example of onAddToSpace successed(2)
Removed scopes script.external_request and chat. And removed "UrlFetchApp.fetch".
Added the bot. onAddToSpace is executed from the beginning. Of course, It was not OAuth.
Posted a message. onMessage successed.
I'd like to get onAddToSpace to be executed at the point of successed(1)-Step2.
I don't know how I can make this work.
Update3
Manifest(modified):
{
"timeZone": "Asia/Tokyo",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"chat": {
"addToSpaceFallbackMessage": "情シスへ質問、依頼ができるbotです。送信したメッセージは、ユーザー名付きですべて情シスチームチャットに送信されます。遊びが一切ないbotです、ご利用の際は十分ご注意ください。ここは万事屋ではありません。"
}
}
For correct functioning of your code you need to include the necessary OAuth scopes into the manifest
For this please modify your manifest as following:
{
"timeZone": "Asia/Tokyo",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"oauthScopes": ["https://www.googleapis.com/auth/chat", "https://www.googleapis.com/auth/script.external_request"],
"runtimeVersion": "V8",
"chat": {
}
}
UPDATE
I understand that you want to show a custom message before configuring your bot
This is not possible for security reasons
It is part of the Google OAuth authenticatication concept that your bot cannot anything (not even writing a message) before being authorized by the user
As states int he documentation
onAddToSpace(e)
This function is executed when your bot is added to a space.
Before you authorize the access you bot is NOT added to a space
WORKAROUND
You can customize the OAuth2 concent screen in your GCP console
Unfortunately there is no way to add a custom message to the consent screen, but you can include / set it as the Application name:
After doing so, your content screen will look as following:
HOW TO TEST
In order to trigger authorization again
Go on https://myaccount.google.com/security
Third party apps with account access
Remove your bot
Now you can test authorization with the new consent screen
Related
I am trying to read deployment information (version number and description) of google sheets addon during runtime using java script.
I found this web app related post. I am not sure whether it is different as my GAS is not web app.
However I also tried this API here and it looks OK, I got all the script deployments info when supplying only the script ID.
I modified appsscript.json to include all script required scopes (until so now it looks like that
{
"timeZone": "Asia/Jerusalem",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/script.deployments",
"https://www.googleapis.com/auth/script.deployments.readonly",
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/script.container.ui",
"https://www.googleapis.com/auth/spreadsheets"
]
}
this is the call to the API
function getDeployments(){
let OAuthToken = ScriptApp.getOAuthToken();
let id = ScriptApp.getScriptId();
var url = 'https://script.googleapis.com/v1/projects/'+ id + '/deployments';
var options = { "muteHttpExceptions": true,
"headers": {
'Authorization': 'Bearer ' + OAuthToken
}
}
var response = UrlFetchApp.fetch (url, options);
console.log( "util.getDeployment" ,response.getResponseCode())
}
but this call returns http code 403.
Please advice
#Tanaike guiding question helped a lot.
Checking the response.getContentText() as opposed response.getContent() I checked before made the issue clear. The latter returned array of numbers, whereas the response.getContentText() returns readable string.
Problem was the App script API was has not been enabled. Enabling this API solved the problem.
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.
I have Google Apps Script-based software deployed to multiple users via Google Cloud Platform. Most of it works via triggers running every x minutes. However, currently there is one script that runs by being called through Apps Script API.
I made some changes yesterday. See this Stack Overflow question that I was able to answer: Google Apps Script API Authorizations for DriveApp.getFileById. (This is for a second script being called via API, but hasn't been deployed yet much.)
This may have "broken' the ability for just two recently onboarded users (the last two onboarded several days ago) to call the script through Apps Script API. The other users are fine. I have compared everything on the client side for all users, and all users seem to be the exact same.
Here is the code on the client side (in runOnEdit) that calls the Apps Script API:
var token = ScriptApp.getOAuthToken();
var header = {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
};
var parms = [id];
var data = {
"function": "checkSheet",
"parameters": parms,
"devMode": false,
}
var options = {
"method":"POST",
"headers": header,
"muteHttpExceptions": true,
"payload": JSON.stringify(data)
};
var response = UrlFetchApp.fetch(url, options);
For the manifest / appsscript.json file, I have:
{
"timeZone": "America/New_York",
"dependencies": {
},
"webapp": {
"access": "MYSELF",
"executeAs": "USER_DEPLOYING"
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/script.external_request"
]
}
The oauthScopes section was just added yesterday for all users (one's working & one's not) - the answer to my Stack Overflow question referenced above. I have also played with adding a number of different oauthScopes for the two users that are affected, but it hasn't helped.
For all users where this is still working, on their client side (where the code just above is located), there are NO Enabled APIs in their GCP Project. See this image:
[![enter image description here][1]][1]
Their OAuth consent screens are just the default - Type: Public, default scopes, etc.
However, this morning for the 2 users that aren't working, when the above code runs and calls the Apps Script API script, the response was:
response = {
"error": {
"code": 403,
"message": "Apps Script API has not been used in project 746982115040 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/script.googleapis.com/overview?project=746982115040 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
"status": "PERMISSION_DENIED",
"details": [
{
"#type": "type.googleapis.com/google.rpc.Help",
"links": [
{
"description": "Google developers console API activation",
"url": "https://console.developers.google.com/apis/api/script.googleapis.com/overview?project=746982115040"
}
]
},
{
"#type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "SERVICE_DISABLED",
"domain": "googleapis.com",
"metadata": {
"consumer": "projects/746982115040",
"service": "script.googleapis.com"
}
}
]
}
}
However, that is no different than the users that are working - they don't have the Apps Script API enabled.
So, I enabled the Apps Script API for one of the two users that this was happening to. Now, after doing that, I get the following message:
response = {
"error": {
"code": 403,
"message": "The caller does not have permission",
"status": "PERMISSION_DENIED"
}
}
Any help is greatly appreciated! Thank you!
First, I have to say that learning GCP and all of the authorizations around it is difficult. There are so many things that could be the reason for this type of problem.
In my case, it ended up being that the "client's" app script project was still an "Apps Script-managed Cloud Platform project" instead of the regular GCP project that the API is associated with.
To fix, inside of the Google Apps Script code editor for the script that is making the API call, go to menu / Resources / Cloud Platform project.
Find the GCP project number you want to associate this with - In my case it is the "server's" project number, from which I have deployed the API.
Click on the "Set Project" button.
Now my API call works!
I have a Google Cloud Platform Project (GCP) that runs almost all scripts/functions directly, but a couple through an API. Everything is only accessible internally to the G Suite domain, and the OAuth consent screen Application Type is "Internal".
I have a script, which is not called via an API, but directly on a timer every x minutes. It performs a
DriveApp.getFileByID(pictureID)
This works great! No problems.
I also have a different script, in the same GCP Project, which instead of running triggered by a timer, runs by being called through an API. Here is the line of code that calls it (not really important):
var result = UrlFetchApp.fetch(url, options);
The API script runs great! Until it gets to the following lines:
try { var file = DriveApp.getFileById(pictureID); }
catch (e) {
Logger.log('e = ' + JSON.stringify(e));
return;
}
The result of the log is
e = {"name":"Exception"}
I verified that the pictureID is the same as it is in the non-API script that works. I am doing a "try" in this API-run script to make sure that the software has access to the file, not to actually access it.
I am pretty sure that this is an authorization issue. I've been using GCP only for a little while now, have some experience with authorizations, but not a lot.
Here are some details around the authorizations...
The project's scripts Project Properties (File/Project Properties) shows that it needs the following OAuth Scopes:
According to Google's documentation at https://developers.google.com/apps-script/reference/drive/drive-app#getfilebyidid,
Scripts that use this method require authorization with one or more of the following scopes:
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/drive
Here are the scopes that I've defined now on the GCP Oauth consent screen:
As you can see, I've added drive, drive.readonly, & drive.file (which doesn't really seem to be needed).
On top of it all, this particular image file is stored in the Google Drive of the owner of the GCP Project, scripts, and top-level admin of the G Suite domain. That will not always be the case, as users will be sharing images from their own Google Drive to this software/GCP owner. However, I have a feeling that even now the script triggered by a timer would work with those user-shared files, but not the script called through an API.
I'm pretty sure this is an Auth issue, but I am missing something somewhere.
Thank you for your help!
Update:
Here is the code from the script that CALLS the API script (changed some for confidentiality), as I'm wondering if perhaps the problem may not be on the client/calling side. Perhaps I'm not getting the OAuthToken correctly? Or the token doesn't have the correct permissions?
var token = ScriptApp.getOAuthToken();
var header = {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
};
var parms = [id];
var data = {
"function": "updateSettings",
"parameters": parms,
"devMode": true,
}
var options = {
"method":"POST",
"headers": header,
"muteHttpExceptions": true,
"payload": JSON.stringify(data)
};
// Call the API
var result = UrlFetchApp.fetch(url, options);
I figured it out after contacting Google, and unfortunately not getting much help, and a few more hours of trying and research...
What I was missing was adding "oathScopes" to the manifest/appsscript.json file. I played around with the scopes needed and ended up with the following two shown below. I figured out the solution by looking at this answer: Using AuthToken obtained via ScriptApp.getAuthToken() to call web apps in GAS.
Inside the script editor, go to View/Show manifest file.
Here is what it looked like before:
{
"timeZone": "America/New_York",
"dependencies": {
},
"webapp": {
"access": "MYSELF",
"executeAs": "USER_DEPLOYING"
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
and here is what it looks like now...
{
"timeZone": "America/New_York",
"dependencies": {
},
"webapp": {
"access": "MYSELF",
"executeAs": "USER_DEPLOYING"
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/script.external_request"
]
}
After adding, I saved appsscript.json. I then went to my runOnEdit trigger (the calling script), removed and re-added it. However, that caused a nasty looking error:
I looked that up, and found this post: Receiving error when creating new trigger on Apps Script. So, I ran the code in the script editor, and indeed it brought up the auth screen. I approved, re-added the trigger, and now everything works great in calling the API!
I created with Apps Script (runtime V8) a Google Chat Bot, that has a backend action triggered by a card; the action is a simple UrlFetchApp call.
Because of this, the bot is not immediately able to respond and so, after a manual input from the user, the configuration card shows up in the user chat.
The problem is that straight after having provided all the authorizations to the pop-up, the onMessage trigger fails (see image below).
I tried in all ways to debug, but the only (odd) thing I found is that
onMessage is called 3 times in a row;
all these calls are failing, with an execution time of 0s (so I suspect the failure happens somewhere else, not due to the function's code);
there is no log at all.
the dummy onMessage function I'm using to debug
function onMessage(object_event) {
try{
console.log(object_event);
return {
text:'some text here'
}
}catch(error){
console.log(error)
}
}
and here is the manifest
{
"timeZone": "Europe/Bucharest",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"oauthScopes": ["https://www.googleapis.com/auth/script.external_request"],
"runtimeVersion": "V8",
"chat": {
"addToSpaceFallbackMessage": "Hello!\nTo get started, please send me a message and then click on the \"Configure\" button that will appear"
}
}
Am I doing something wrong, or is this a bug?