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?
Related
I have a Google sheets add-on listed on the market.
In most cases it works OK, but for something like 10% of the users I can see in the Log Explorer the following security Notice level entry:
{
"insertId": "-95cptdf17zlkw",
"jsonPayload": {
"serviceContext": {
"service": "AKfycbwdlQO2SpUoJE-rL-QMnni4dihrhsvrx16GwESJswoy8NA"
},
"message": "Authorisation is required to perform that action."
},
"resource": {
"type": "app_script_function",
"labels": {
"project_id": "formula-tracer-side-bar",
"invocation_type": "unknown",
"function_name": "excuteUserRequest"
}
},
"timestamp": "2022-05-17T15:56:10.170Z",
"severity": "NOTICE",
"labels": {
"script.googleapis.com/process_id": "EAEA1GOyaeEOdACtJlc76JEW70I8J5yujif1YQ8poDnneE6Tlg85jwUqn_oSIWZygqSxQ6s6DPtMt4jkIVo6Ak-2W8lR0K6Eyldw8kTvxvn-dauNu4PbQ8cgbpvGcReRquwiaOeV-ZehZev1w-1gmujKdpnPk95NERrAmskkifGIhFhjfmKrcb_wIdJENMD_ww4kgZWxiMJ0VSmNsV-Qr2d_FFoek6tF09xXJMgtIHBVWLEG9aJrn7GLfOfebJqj4L_RXWu1xu4aSKFiayU-HcJH_8-Yuo8FSMyIeC_Qd",
"script.googleapis.com/project_key": "Mzy7e87MSzO0enTUViEAdROLp-GB2FQma",
"script.googleapis.com/deployment_id": "AKfycbwdlQO2SpUoJE-rL-QMnni4dihrhsvrx16GwESJswoy8NA"
},
"logName": "projects/formula-tracer-side-bar/logs/script.googleapis.com%2Fconsole_logs",
"receiveTimestamp": "2022-05-17T15:56:10.579606416Z"
}
Relevant add-on pseudo code:
Server
function onInstall() {
onOpen();
}
//add menu items to spreadsheet
function onOpen() {
let menu = SpreadsheetApp.getUi().createAddonMenu();
menu.addItem('Show side bar', 'showSidebar');
menu.addToUi();
}
// called by menu items
function showSidebar() {
let template = HtmlService
.createTemplateFromFile("sideBar");
let htmlOutput = template.evaluate();
htmlOutput.setTitle(getHTMLTitle());
SpreadsheetApp.getUi().showSidebar(htmlOutput);
}
//called by client
function excuteUserRequest(param){
console.info("excuteUserRequest",param);
//handle client request
processRequest()
}
Client
//client
window.onload = function(){
/* do some html initializtions*/
initPage();
//call server when addon is launched
sendRequest();
}
function sendRequest(){
let param = getParam();
//Call trace on server
google.script.run
.withSuccessHandler(onSuccessRequest)
.withFailureHandler(onFailueRequest)
.excuteUserRequest(param);
}
The server call to excuteUserRequest in the client sendRequest() is missing permissions. Looks like that the user(s) is not recognized by google as the error entry (although is logger entry is Notice level) is missing the user_key information as in most log entries
Add-on is listed and approved after it successfully passed the oAuth verification process.
So as I can understand it - the user approved the required permissions the add-on asks while installing
I can not reproduce this situation. I thought it might be something with the user permissions for the specific spreadsheet the user is using. However, when using spreadsheets with no edit permission, the Google Sheets extensions menu is disabled thus the addon can not be launched at all.
this post looks similar (client call app script server function sometimes fails with missing authorization), but I do not really understand if it is actually triggered by the same cause and if so, what to do.
Other posts (this one and this one) looks irrelevant as they refer executing app script function - not one called from client, suggesting to manually run the function in the script editor to get the relevant permissions.
some clarification:
user_key like actually all log fields accept the jsonPayload.message are added to the log entry. for example - the following log entry is from within showSidebar() the one before the error message.
the log message is only "X.XXX#YYYY.com showSideBar count: 0"
where:
X.XXX#YYYY.com is the user email added by the addon for any console.log().
all other log fields are added by google for each console.log(msg).
this includes the user_key which is assigned by Google.
To shortly summarize:
So same user first call showSideBar() from the spreadsheet extensions menu. This works fine, but the second call triggered immediately fails.
{
"insertId": "bxz0h0f1zmrbn",
"jsonPayload": {
"serviceContext": {
"service": "XXX",
"version": "27"
},
"message": "X.XXX#YYYY.com showSideBar count: 0"
},
"resource": {
"type": "app_script_function",
"labels": {
"function_name": "",
"invocation_type": "menu",
"project_id": "formula-tracer-side-bar"
}
},
"timestamp": "2022-05-17T15:56:12.864Z",
"severity": "INFO",
"labels": {
"script.googleapis.com/project_key": "AAA",
"script.googleapis.com/process_id": "BBB",
"script.googleapis.com/deployment_id": "CCC",
"script.googleapis.com/user_key": "DDD"
},
"logName": "EEE",
"receiveTimestamp": "2022-05-17T15:56:13.653216430Z"
}
Please advise
I found the reason for that, and how to recreate it.
There is actually a pretty old issue for that in issues tracker data base
This is happening whenever:
A client is simultaneously logged in to multiple Google's accounts
the current active account (e.g. Account1) is not the default one (e.g. Account2)
In this scenario the server call is performed under the authorization of Account2 even though the active account is Account1
This is actually happen with Google services themselves.
for example:
When trying to open the script editor from the spreadsheet.
You can see in the image that even though the spreadsheet is shared by document owner account Formula Tracer, with account Formula Analyzer (default account) with full permissions, trying to open the script editor under the authority of Formula Tracer (the owner and active account) fails.
In the question specific case
function sendRequest(){
let param = getParam();
//Call trace on server
google.script.run
.withSuccessHandler(onSuccessRequest)
.withFailureHandler(onFailueRequest)
.excuteUserRequest(param);
}
function onFailueRequest(error){
}
the failure handler onFailueRequest is called with one of the two error messages:
authorization is required to perform this action
We're sorry, a server error occurred while reading from storage. Error code PERMISSION_DENIED
Note:
The 2nd possible message is localized so is will come based on client language. only the suffix PERMISSION_DENIED is in English
The type of error parameter might be a string or Error object
I have been using https://github.com/labnol/apps-script-starter as a starter guide to create app script projects for Google Slides.
But I came across an issue related to the context (active user) that the appscript function is called. While calling the method Session.getActiveUser().getEmail() in the onOpen() function I get the correct active account but when I call the same function from a templated HTML I get the default account for that browser. Has anyone faced this issue? A solution will be great.
const onOpen = (e) => {
Logger.log(Session.getActiveUser().getEmail());
Logger.log(Session.getEffectiveUser().getEmail());
..
}
const customFunction = () => {
Logger.log(Session.getActiveUser().getEmail());
Logger.log(Session.getEffectiveUser().getEmail());
..
}
const showSidebar = () => {
const template = HtmlService.createTemplateFromFile('gra-main-v3');
template.include = include;
const html = template.evaluate().setTitle('New Sidebar');
SlidesApp.getUi().showSidebar(html);
};
gra-main-v3.html
<HTML>
...
<script>
google.script.run.customFunction();
</script>
..
</HTML>
I have two google accounts logged in to the same browser emailA#gmail.com (default) and emailB#gmail.com. When using the default account everything works as expected, but while using the second account the custom function called from the templated HTML gives the wrong result.
Case 1 When emailA#gmail.com is active:
Results in onOpen = emailA#gmail.com
Results in customFunction = emailA#gmail.com
Case 2 When emailB#gmail.com is active:
Results in onOpen = emailB#gmail.com
Results in customFunction = emailA#gmail.com
appscript.json
{
"timeZone": "Asia/Calcutta",
"runtimeVersion": "V8",
"dependencies": {
"enabledAdvancedServices": [],
"libraries": []
},
"webapp": {
"access": "ANYONE",
"executeAs": "USER_ACCESSING"
},
"exceptionLogging": "STACKDRIVER",
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/script.container.ui",
"https://www.googleapis.com/auth/presentations.currentonly",
"https://www.googleapis.com/auth/presentations",
"https://www.googleapis.com/auth/userinfo.email"
]
}
This is a multi-account sign-in issue. Please navigate to the corresponding issue on issue tracker and star it, so that Google knows it's important to solve.
Meanwhile a couple of work-arounds have been proposed, which both boil down to alerting the user that there is a multi-account issue:
By Romain Vilard
Comment 117 in the tracker.
The idea is to compare the effective user to the current user, or a document owner to the current user, and if they are not the same, then alert the user that they need to sign out out of other accounts.
That's the best we can do for now.
From the documentation:
getEffectiveUser():
Gets information about the user under whose authority the script is running. If the script is a web app set to "execute as me" (the developer), this returns the developer's user account. If the script is running under an installable trigger, this returns the account of the user who created the trigger. In most other scenarios, this returns the same account as getActiveUser().
So when you call the function from templated HTML, it return the account that own this script (emailA#gmail.com I guess).
You can set Execute as to `User accessing the web-app' if necessary (also reffer to add-on).
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'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
I have the following line of code in my apps script file, followed by a basic onOpen() function to build a menu:
var listContainers = TagManager.Accounts.Containers.list('accounts/' + accountId);
When that line of code is commented out, the menu is created. But if that line is not commented out, the menu is not created, and the script doesn't run.
When I check the execution transcript (View --> Execution transcript), I see the following error associated with the line shown above:
Execution failed: Login Required
Under Resources-->Advanced Google Services, I have turned on the Tag Manager API, and I have also enabled that API in the Google API Console. The accountId is one for a GTM account that I am the owner of.
This is what my manifest file looks like (View --> Show manifest file):
{
"timeZone": "America/Mexico_City",
"oauthScopes": [
"https://www.googleapis.com/auth/script.container.ui",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/tagmanager.delete.containers",
"https://www.googleapis.com/auth/tagmanager.edit.containers",
"https://www.googleapis.com/auth/tagmanager.edit.containerversions",
"https://www.googleapis.com/auth/tagmanager.manage.users",
"https://www.googleapis.com/auth/tagmanager.publish"
],
"dependencies": {
"enabledAdvancedServices": [{
"userSymbol": "TagManager",
"serviceId": "tagmanager",
"version": "v2"
}]
},
"exceptionLogging": "STACKDRIVER",
"executionApi": {
"access": "ANYONE"
}
}
I feel like all of my grounds are covered here, so I'm not understanding why it is giving me that Login Required error. I have googled up-and-down for a solution, and can't find anything indicating I am doing anything wrong.
Any ideas would be greatly appreciated...thanks!
Global variables can't access external services that require authorization in the same way that simple triggers like onOpen and onEdit as well as custom functions can't access them.
If you really want that listContainers be a global variable You could initialize it at the global scope by doing something like
var listContainers;
Then on a function called by a user interface or an installable trigger assign the tag manager list to that variable in the following way:
listContainers = TagManager.Accounts.Containers.list('accounts/' + accountId);