Gmail API in GAS: Refreshing Interface on State Change? - google-apps-script

I'm currently writing using Google-Apps-Scripts to do change labels on threads and messages. When the script completes, the payloads are moved, but the display on the user frontend does not change until the user interacts with Gmail.
Is there a way to push a refresh command to Gmail? How can I gracefully display "job's done" to the user so they know that messages are now appropriately labeled?
I am working directly against the Gmail API, not GmailApp. I discovered ActionResponseBuilder.setStateChanged(), but the problem is I am currently not working with any sort of frontend interface. This is all in the background.
Here is an abbreviated example of some of the code I'm using to grab messages to modify (as requested):
function changeLabel(ids,oldLabel,newLabel) {
Gmail.Users.Messages.batchModify({
"ids":ids,
"addLabelIds":oldLabel,
"removeLabelIds":newLabel
},"me");
}
function start() {
// Labels to work with
const FOO = "Label_5095729546757874255";
const BAR = "Label_5102306845672214551";
// API call to retrieve list of messages by Label
var msgIdsByLabel = new MessageIndex(FOO);
// API call to retrieve message contents
var payloadMessages = new Messages(msgIdsByLabel);
var manifestMessagesToMove = [];
for (var i=0;i < Object.keys(payloadMessages.response).length; i++) {
// Criteria for selecting messages to move goes here
manifestMessagesToMove[i] = payloadMessages.response[i].id;
}
// Change labels of Message Ids
changeLabel(manifestMessagesToMove,FOO,BAR);
// ??? Refresh Gmail Interface ???
}

Unfortunately this isn't possible.
The Gmail UI can't be refreshed from Apps Script as it is run in the cloud as a separate session to that which is being viewed in the web browser by a user. The two aren't connected - and the same goes for the Gmail API.
If you don't have a front-end interface (aka a Gmail Add-on utilising CardService) then there is not a way of displaying a message to the user, either. The refresh will have to be done manually.

Related

Why is doGet() failing without posting logs?

I suppose my question is twofold: doGet() in the following context will just fail after 0.1~0.2 seconds without posting logs, so I have no idea how to troubleshoot it by myself. Additionally, if I'm having the script execute on my behalf, do I have to push a request with my authorization token to a more "pertinent" area than just the sheet name, such as within the iteration itself? Read further for more details:
I have a source spreadsheet where I am cross-referencing user inputted data to validate the information we have "on file". Most of our clients are over the age of 55, so I am trying to reduce end-user complexity by having the script run on my behalf whenever they need to use it (to bypass the Authorization screen, with the big scary "This application could be unsafe!" message). The way I've read to accomplish this seems to be with doGet(), so I set up a low-level HTTP Get request that just pushes a doGet() with my OAuth token, returning the sheet name. I also set up a masking function specifically to do this, and linked it to the button originally used for the iteration logic. The doGet() looks like this:
const doGet = e => {
Logger.log(`Recieved HTTP request.`);
const content = ContentService.createTextOutput(iterator(e));
Logger.log(content);
return content;
}
and the button that uses UrlFetchApp looks like:
const runMask = () => {
const active = SpreadsheetApp.getActiveSheet().getSheetName();
const v4 = 'https://script.google.com/macros/s/<scriptid>/dev' // ScriptApp.getService().getUrl() posts 404
UrlFetchApp.fetch(`${v4}?sheetName='${active}'`, {
headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` },
});
I have some logs set up within the real runMask() that proceed all the way to the end of the program, giving me real URLs and OAuth tokens, so I know it's making it through runMask() without an issue. However, the doGet() log doesn't post anything, even at the top of the function. I can see that it's executing the trigger in my execution log, but the log itself remains empty.
I've tried:
using ScriptApp.getService().getUrl() in place of v4: posts 404 in the log w/ truncated server response
replacing ${active} with the name of the sheet: same issue; logging ${active} also returns the correct name of the sheet.
Beyond this, I'm not even sure what to do. I have everything scoped correctly (auth/spreadsheets.currentonly, auth/script.external_request, and auth/userinfo.email), and I have no issues about operational security (as both the spreadsheet and script are written by me, the clients have no need to grant access to their entire drive). Before trying to implement doGet() and bypass the authorization screen, the iterator itself worked just fine. As such, I have chosen not to include it here, as it's hardly relevant (the function that executes the iteration function never makes it to that point).
I understand this has been quite the deluge of information; I'd be happy to provide more information or context as needed.
Getting ReferenceError: iterator is not defined (line 12, file "ag2")
With this:
const doGet = e => {
Logger.log(`Recieved HTTP request.`);
const content = ContentService.createTextOutput(iterator(e));
Logger.log(content);
return content;
}
Issued with url/exec?option=A
It runs with
const doGet = e => {
Logger.log(`Recieved HTTP request.`);
const content = ContentService.createTextOutput(JSON.stringify(e));
Logger.log(content);
return content;
}
and returns the appropriate stringified object
Only use the test URL (/dev) for testing the web app from a web browser.
Before doGet from a web browser using a versioned deployment (/exec) remember to publish a new version.
Assign a Google Cloud Project to your Google Apps Script project. For details see https://developers.google.com/apps-script/guides/cloud-platform-projects.
To make it easier to debug your avoid calling functions from a Google Apps Script method like createTextOutput, instead, assign the function result to a variable and use it as the method parameter, i.e. replace
const content = ContentService.createTextOutput(iterator(e));
by
const something = iterator(e);
const content = ContentService.createTextOutput(something);
For debugging purposes, create a function to call your doGet function, and check that it hasn't any problem to run, i.e.
function __test__doGet(){
const e = {
parameter: {}
}
doGet(e);
}
Related
Exception handling in google apps script web apps
Issue:
When I saw your question, I'm worried about I have everything scoped correctly (auth/spreadsheets.currentonly, auth/script.external_request, and auth/userinfo.email).
If you are using only the following scopes at oauthScopes of appsscript.json,
https://www.googleapis.com/auth/spreadsheets.currentonly
https://www.googleapis.com/auth/script.external_request
https://www.googleapis.com/auth/userinfo.email
Unfortunately, these scopes cannot be used for access to Web Apps. Although I'm not sure about the method for running your function of runMask, I thought that this might be the reason for your issue.
Solution:
If you want to access Web Apps of https://script.google.com/macros/s/<scriptid>/dev using the access token retrieved by ScriptApp.getOAuthToken(), please include the following scope.
https://www.googleapis.com/auth/drive.readonly
or
https://www.googleapis.com/auth/drive
After you include the above scope, please reauthorize the scopes, and test it again. When your function of iterator has already been declared and the script worked, by running runMask, you can see the log of Logger.log(Recieved HTTP request.) and Logger.log(content) at the log.
Reference:
Taking advantage of Web Apps with Google Apps Script

GAS userProperties not behaving as expected

In the guide to Properties Service, it is stated that the User Properties is for data specific to the current user of a script. I have a stand alone app deployed for the user accessing the app. I have used PropertiesService.getUserProperties() to set some values. And I expect those values be specific to each user; however, every time a user opens the app, the values for that user overwrites the ones for other users. Is this a bug or expected behavior?
Minimal Reproducible Example:
function doGet(e) {
const userProps = PropertiesService.getUserProperties();
let userData = JSON.parse(userProps.getProperty('userData'));
if (!('foo' in userData)) {
userData = {foo: e.parameters.userData}
userProps.setProperty('userData', JSON.stringify(userData));
}
return HtmlService.createHtmlOutput(`<p>Expected data specific to ${Session.getActiveUser().getEmail();}:</p><p>${userData.foo}`);
}
Expected behavior:
When deployed as a stand-alone app configured to execute for user accessing the web app, the displayed data must be specific to the user.
Observed behavior:
When a new user runs the app, previous users see the data set by the new user, as if the new user's setting has overwritten theirs.

Pulling current price change % (Google Finance) with Google Apps Script

I'm trying to pull current price change % from Google Finance with Google Apps Script. With the following coding, I couldn't figure out why it doesn't pull current price change % (0.72%), though it retrieves "After Hours" price change % (0.081%). Can anyone help me out? Thank you!!
function test() {
var url = 'https://www.google.com/finance/quote/AAPL:NASDAQ';
var res = UrlFetchApp.fetch(url, { muteHttpExceptions: true }).getContentText();
var location = '', sub = '', index = [], price = '', change = [];
// From google finance, scrape whole plain string from first <div class="YMlKec fxKbKc"> tag where current price is.
location = '<div class="YMlKec fxKbKc">';
index = res.indexOf(location);
sub = res.substring(index + location.length, index + location.length + 1000);
// Logger.log(sub);
// Pull current price from the sub variable. This is succesful.
price = sub.substring(0, sub.indexOf('<'));
Logger.log(price);
// Pull current price change, which should be 0.72%. But it didn't work.
// This retrieved only "After Hours" price change %, 0.081%, but not 0.72%, current price change % that I'm looking for.
location = '%';
index = [sub.indexOf(location)];
for (var i = 0; i < index.length; ++i) {
change = sub.substring(index[i] - 5, index[i] + 1);
Logger.log(change)
}
}
Is it possible to use Google Apps Script to get an info from the code I see at DevTools?
First thing that you should be aware is that the code shown in the Elements tab of Chrome Developers Tools is the current DOM but UrlFetchApp.fetch is only able to get the code from the source file referred directly by the URL.
To review the source code you might right click on the webpage and select View page source or go open the file from the Chrome Developers Tool's Sources tab. If the value that you want to read is not there, the best is to use a stock market API, otherwise you have to implement a headless browser.
There have being a several questions about getting data from Google Finance. Most of the question that involves Google Apps Script are related to using the GOOGLEFINANCE built-in function in Google Sheets, where most of the OP didn't reviewed the official help article about GOOGLEFINANCE. From this doc:
Historical data cannot be downloaded or accessed via the Sheets API or Apps Script. If you attempt to do so, you'll see a #N/A error in place of the values in the corresponding cells of your spreadsheet.
The relevance of the above is that Google is taking some measures to prevent that certain data be retrieved automatically.
In order to make Google Apps Script able to "see" what is shown in the web browser using only server-side code you should have to implement a headless browser:
A headless browser is a web browser without a graphical user interface.
Headless browsers provide automated control of a web page in an environment similar to popular web browsers, but they are executed via a command-line interface or using network communication. They are particularly useful for testing web pages as they are able to render and understand HTML the same way a browser would, including styling elements such as page layout, colour, font selection and execution of JavaScript and Ajax which are usually not available when using other testing methods.
Another alternative might be to u use client-side code as the HTML Service could be used to create a dialog/sidebar/web-application to serve it including google.script API support. The later could be used to send to the server-side code some data from the client-side with certain limitations, i.e. Date, Function and DOM objects can't be sent to server side but you might pass them as JSON.
Related
How can I get stock quotes using Google Finance API?
Using Apps Script to scrape javascript rendered web page
Can we use Chrome V8 functionalities from GAS?
You can get current price change using axios and cheerio, the code below shows you how to do this. And check the code in the online IDE
const cheerio = require("cheerio");
const axios = require("axios");
const currencyName = "AAPL:NASDAQ"; // currency name from browser URL
const BASE_URL = "https://www.google.com/finance/quote/";
const AXIOS_OPTIONS = {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36",
}, // adding the User-Agent header as one way to prevent the request from being blocked
};
function getCurrentPrice() {
return axios(BASE_URL + currencyName, AXIOS_OPTIONS).then(({ data }) => {
const $ = cheerio.load(data);
const pattern = /data:\[{3}(?<data>\[.{100,500}\])\]{3}, side/gm //https://regex101.com/r/VLKbBt/1
const currencyData = JSON.parse([...data.matchAll(pattern)].map(({groups}) => ({data: groups.data})).find(el=> el.data.includes(currencyName)).data);
const priceChange = currencyData[5][2]
return priceChange;
});
}
getCurrentPrice().then(console.log);
Output:
-0.53461844
And as you can see in the screenshot below, the data that we received is exactly as on the page.
If for some reason you want to make a Google Finance page parser in Python, have a look at the dedicated Google Finance blog post at SerpApi.
Disclaimer, I work for SerpApi

Building a card and updating it after fetching data in Google Apps Script

I am trying to build a Gmail addon which includes 2 external API calls. The first one is fast (~200ms) and the second one is slow (~5s). Because of this I would like to first build the card with the results of the first fetch, and then update the card after the second call finishes.
Would it be possible to either:
Call fetchAll and build and render the card each time a request finishes
Trigger a function after the initial rendering is done (after return card.build())
Update the root card without returning it (I tried CardService.newNavigation().popToRoot().updateCard(card.build()) without success)
Any preferred way to render a card and then update it after data is fetched would be appreciated!
Below is an example function if useful.
function onGmailMessage(e) {
// Fetching email
var messageId = e.gmail.messageId;
var accessToken = e.gmail.accessToken;
GmailApp.setCurrentMessageAccessToken(accessToken);
var message = GmailApp.getMessageById(messageId);
// Preparing requests
var data = {
'text': message.getPlainBody(),
};
var options = {
'method' : 'post',
'contentType': 'application/json',
'payload' : JSON.stringify(data)
};
// Fetching responses. Here I would love to first display
// createCard(response_1) and then when the second call finishes
// return createCard(response_1 + '/n' + response_2)
var response_1 = UrlFetchApp.fetch('http://API_1/', options);
var response_2 = UrlFetchApp.fetch('http://API_2/', options);
return createCard(response_1 + '/n' + response_2);
Answer:
Unfortunately, this is not possible to do.
More Information:
This is a bit tricky so I'll split this answer down into your three points:
[Is it possible to] call fetchAll and build and render the card each time a request finishes?
A fetchAll function could be made to get all API responses, but you'll still end up waiting for API 2 to respond before updating what can be seen in the card.
The problem with this is that in order to display the rendered card, you need to make a return of some kind. Once you return the response of the first API your second API won't be made at all as the function will have already executed. Which leads onto point two:
[Is it possible to] trigger a function after the initial rendering is done (after return card.build())
I did a test with this, instead of returning API 1's response directly I stored its value in a Script Property and made a trigger execute 200 ms later with the call to API 2:
function onGmailMessage(e) {
// previous code
var response_1 = UrlFetchApp.fetch('http://API_1/', options);
ScriptApp.newTrigger("getSecondResponse").timeBased().after(200).create();
PropertiesService.getScriptProperties().setProperty('response1', response_1);
return createCard(response_1);
}
function getSecondResponse() {
// options 2 definition here;
var response_1 = PropertiesService.getScriptProperties().getProperty("response1");
var response_2 = UrlFetchApp.fetch('http://API_2/', options);
return createCard(response_1 + '/n' + response_2);
}
and adding the correct scopes in the manifest:
{
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/script.locale",
"https://www.googleapis.com/auth/gmail.addons.current.action.compose",
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://mail.google.com/",
"https://www.googleapis.com/auth/script.scriptapp"
]
}
And which this did call the first API, display the response in the card and make the trigger, the card didn't update. I presume this is because the trigger acts as a cron job being executed from somewhere which isn't the add-on itself, so the second card return is never seen in the UI.
[Is it possible to] update the root card without returning it (I tried CardService.newNavigation().popToRoot().updateCard(card.build()) without success)
updateCard() is a method of the Navigation class. There's a whole page in the documentation which details the uses of Card navigation but the important parts to take away here is that the navigation methods are used in response to user interaction. From the documentation:
If a user interaction or event should result in re-rendering cards in the same context, use Navigation.pushCard(), Navigation.popCard(), and Navigation.updateCard() methods to replace the existing cards.
The following are navigation examples:
If an interaction or event changes the state of the current card (for example, adding a task to a task list), use updateCard().
If an interaction or event provides further detail or prompts the user for further action (for example, clicking an item's title to see more details, or pressing a button to create a new Calendar event), use pushCard() to show the new page while allowing the user to exit the new page using the back-button.
If an interaction or event updates state in a previous card (for example, updating an item's title from with the detail view), use something like popCard(), popCard(), pushCard(previous), and pushCard(current) to update previous card and the current card.
You can create multiple cards which have different content - for example one which contains response_1 and one which contains response_1 + "\n" + response_2, but some kind of interaction from a user is still needed to switch between the two views, and it won't get around the wait time you need to get a response from API 2.
Feature Request:
You can however let Google know that this is a feature that is important and that you would like to request they implement it. Google's Issue Tracker is a place for developers to report issues and make feature requests for their development services. I would suggest using the feature request template for G Suite Add-ons for this, rather than Apps Script directly.
References:
Class Navigation | Apps Script | Google Developers
Card navigation | G Suite Add-ons | Google Developers

Trigger Google Apps Script by email

I'm looking for examples of a pattern where a demon script running within a GoogleAppsForBusiness domain can parse incoming email messages. Some messages will will contain a call to yet a different GAScript that could, for example, change the ACL setting of a specific document.
I'm assuming someone else has already implemented this pattern but not sure how I go about finding examples.
thx
You can find script examples in the Apps Script user guide and tutorials. You may also search for related discussions on the forum. But I don't think there's one that fits you exactly, all code is out there for sure, but not on a single script.
It's possible that someone wrote such script and never published it. Since it's somewhat straightforward to do and everyone's usage is different. For instance, how do you plan on marking your emails (the ones you've already read, executed, etc)? It may be nice to use a gmail filter to help you out, putting the "command" emails in a label right away, and the script just remove the label (and possibly set another one). Point is, see how it can differ a lot.
Also, I think it's easier if you can keep all functions in the same script project. Possibly just on different files. As calling different scripts is way more complicated.
Anyway, he's how I'd start it:
//set a time-driven trigger to run this function on the desired frequency
function monitorEmails() {
var label = GmailApp.getUserLabelByName('command');
var doneLabel = GmailApp.getUserLabelByName('executed');
var cmds = label.getThreads();
var max = Math.min(cmds.length,5);
for( var i = 0; i < max; ++i ) {
var email = cmds[i].getMessages()[0];
var functionName = email.getBody();
//you may need to do extra parsing here, depending on your usage
var ret = undefined;
try {
ret = this[functionName]();
} catch(err) {
ret = err;
}
//replying the function return value to the email
//this may make sense or not
if( ret !== undefined )
email.reply(ret);
cmds[i].removeLabel(label).addLabel(doneLabel);
}
}
ps: I have not tested this code
You can create a google app that will be triggered by an incoming email message sent to a special address for the app. The message is converted to an HTTP POST which your app receives.
More details here:
https://developers.google.com/appengine/docs/python/mail/receivingmail
I havn't tried this myself yet but will be doing so in the next few days.
There are two ways. First you can use Google pub/sub and handle incomming notifications in your AppScrit endpoint. The second is to use the googleapis npm package inside your AppScript code an example here. Hope it helps.
These are the steps:
made a project on https://console.cloud.google.com/cloudpubsub/topicList?project=testmabs thing?
made a pubsub topic
made a subscription to the webhook url
added that url to the sites i own, i guess? I think I had to do DNS things to confirm i own it, and the error was super vague to figure out that was what i had to do, when trying to add the subscription
added permission to the topic for "gmail-api-push#system.gserviceaccount.com" as publisher (I also added ....apps.googleusercontent.com and youtrackapiuser.caps#gmail.com but i dont think I needed them)
created oauth client info and downloaded it in the credentials section of the google console. (oauthtrash.json)