How to catch execution error in XMLService getAttribute method? - google-apps-script

I'm using this script in a Google Sheets spreadsheet to parse a table in a web page and to store results:
var doc = XmlService.parse(result);
var html = doc.getRootElement();
var resulttable = getElementsByClassName(html, 'resulttable')[0];
var descendants = html.getDescendants();
descendants.push(html);
for(var i in descendants) {
var elt = descendants[i].asElement(); <== it crashes
if(elt != null) {
var test = elt.getAttributes();
var test_bis = elt.getAttribute('http-equiv'); <== it does not crashes: 'http-equiv' exists
var classes = elt.getAttribute('class'); <== it crashes:'class' does not exists
}
}
As it's shown, I have some errors (simply raised as "server errors") in the marked lines of this code. I also put try...catch block, but they don't catch the errors: the script terminates abruptly.
How can I catch the errors so as to let the script continuing despite some of these XML errors?
My expectations were to have undefined elements when the asElement or the getAttribute methods fail.
To parse the URL I was using this approach
var url = "https://albopretorio.comune.gravina.ba.it/fo/?ente=GravinaInPuglia";
var today = Utilities.formatDate(new Date(), "GMT+1", "dd/MM/yyyy");
var payload =
{
"tipoSubmit":"ricerca",
"enti":"GravinaInPuglia",
"uo":"",
"tipoatto":"",
"anno":"",
"numda":"",
"numa":"",
"annoatto":"",
"numatto":"",
"datada":"",
"dataa":"",
"pubblicatoda":today,
"pubblicatoa":"",
"presenteal":"",
"chiave":"",
"provenienza":"",
};
var options =
{
"method" : "POST",
"payload" : payload,
"followRedirects" : true,
"muteHttpExceptions": true
};
var result = UrlFetchApp.fetch(url, options);

Issue:
You don't know which ContentType each descendant is, so you don't know which method you should use to cast the node.
Solution:
For each descendant, check for the corresponding ContentType with the method getType(). You can use the returned value (the ContentType) and a switch statement to use one method or another.
Code snippet:
for (var i in descendants) {
var contentType = descendants[i].getType();
var elt;
switch (contentType.toString()) {
case 'TEXT':
elt = descendants[i].asText();
break;
case 'ELEMENT':
elt = descedants[i].asElement();
break;
// Add other possible ContentTypes, if necessary
Update: Issue with getAttribute:
In order to avoid the script trying retrieve an attribute that does not exist, you can retrieve an array of the attribute names for this element, and then check if your attribute is included in that array:
var attributes = elt.getAttributes().map(attribute => attribute.getName());
if (attributes.includes('class')) {
var classes = elt.getAttribute('class');
}
Reference:
Enum ContentType
Interface Content: getType()
switch

Related

Error handling - retry urlfetch on error until success

I've looked at all the relevant questions here (such as this), but still cannot make sense of this VERY simple task.
So, trying to verify numbers using the NumVerify API. We're still on the free license on APILAYER so we're getting the following error from time to time
Request failed for https://apilayer.net returned code 500
I'd like to add a loop so that the script will try again until a proper response is received.
Here is a snippet based on several answers here:
function numverifylookup(mobilephone) {
console.log("input number: ",mobilephone);
var lookupUrl = "https://apilayer.net/api/validate?access_key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&number="+mobilephone+"&country_code=IL";
try {
var response = UrlFetchApp.fetch(lookupUrl);
if (response) {//Check for truthy value
var json = response.getContentText();
} else {
Utilities.sleep(2000);
continue;//If "get" returned a falsy value then continue
}
} catch(e) {
continue;//If error continue looping
}
var data = JSON.parse(response);
Sadly, still not working due to the following error:
Continue must be inside loop. (line 10
Any thoughts?
I think it's actually better to solve this using muteHTTPexepctions but couldn't quite make it work.
Thanks!
I think I got this to work as below:
function numverify(mobilephone);
console.log("input number: ",mobilephone);
var lookupUrl = "https://apilayer.net/api/validate?access_key=XXXXXXXXXXXX&number="+mobilephone+"&country_code=IL";
var i = 0;
var trycount = 1;
var errorcodes = "";
while (i != 1) {
var response = UrlFetchApp.fetch(lookupUrl, {muteHttpExceptions: true });
var responsecode = response.getResponseCode();
var errorcodes = errorcodes + "," + responsecode;
if (responsecode = 200) {//Check for truthy value
var json = response.getContentText();
var i = 1
} else {
var trycount = trycount + 1;
Utilities.sleep(2000);
}
}
var data = JSON.parse(response);
var valid = data.valid;
var localnum = data.local_format;
var linetype = data.line_type;
console.log(data," ",valid," ",localnum," ",linetype," number of tries= ",trycount," responsecodes= ", errorcodes);
var answer = [valid,localnum,linetype];
return answer;
}
I'll circle back in case it still doesn't work.
Thanks for helping!
You cannot use continue to achieve what you want, instead you can / need to call the function again:
function numverifylookup(mobilephone) {
console.log("input number: ", mobilephone);
var lookupUrl = "https://apilayer.net/api/validate?access_key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&number=" + mobilephone + "&country_code=IL";
try {
var response = UrlFetchApp.fetch(lookupUrl);
if (response) {//Check for truthy value
var json = response.getContentText();
} else {
Utilities.sleep(2000);
numverifylookup(mobilephone);
}
} catch (e) {
Utilities.sleep(2000);
numverifylookup(mobilephone);//If error rerun the function
}
var data = JSON.parse(response);
}
As you can draw from the documentation the statement continue can only be used inside of loops, like e.g. the for loop.

Gmail Add-on pass parameters to function triggered by card action

I'm building a Gmail add-on that creates a card with just one form button. Once clicked, I want the button to trigger a function that will send the open email's content to an external API.
So far, I have something like this:
function createCard(event) {
var currentMessage = getCurrentMessage(event).getBody();
var section = CardService.newCardSection();
var submitForm = CardService.newAction()
.setFunctionName('callAPI');
var submitButton = CardService.newTextButton()
.setText('Submit')
.setOnClickAction(submitForm);
section.addWidget(CardService.newButtonSet()
.addButton(submitButton));
var card = CardService.newCardBuilder()
.setHeader(CardService.newCardHeader()
.setTitle('Click this button'))
.addSection(section)
.build();
return [card];
}
function callAPI(event) {
var payload = { "msg": msg }; // msg is the parameter I need to get from the function call
var options = {
"method" : "POST",
"contentType": "application/json",
"payload" : JSON.stringify(payload),
"followRedirects" : true,
"muteHttpExceptions": true
};
return UrlFetchApp.fetch('https://www.someAPI.com/api/endpoint', options);
}
How can I pass the currentMessage variable into the callAPI function? According to the documentation, the only parameter that we can get from an action function seems to be event that only has the form fields data. If there isn't a way to pass other parameters, is there a way for that function to get the context data of the message directly inside the function??
Thanks!
I believe the proper way to pass the currentMessage content to the callAPI function would be to use the setParameters as described in this documentation
Your code would look like this:
var submitForm = CardService.newAction()
.setFunctionName('callAPI')
.setParameters({message: currentMessage});
In the callback, your would fetch the message by using something like:
function callAPI(event) {
var payload = event.parameters.message;
...
As a reminder, both the key (message) as the value (currentMessage) must be Strings.
You can use Properties Service, which allows you to store strings as key-value pairs. The idea would be to store the value of currentMessage in a property in the function createCard, and then use it in callAPI.
First, to store the variable, you can add the following lines to createCard after declaring currentMessage:
var userProperties = PropertiesService.getUserProperties();
userProperties.setProperty("currentMessage", currentMessage);
Second, to retrieve this variable, add the following lines to callAPI:
var userProperties = PropertiesService.getUserProperties();
var msg = userProperties.getProperty("currentMessage");
Update:
In order to avoid the limit of characters for a single property, you could also split your message into as many properties as were necessary.
You would first have to split the message into an array via substring, and store each element of the array in a different property. You would have to add this to createCard, after declaring currentMessage:
var messageArray = [];
var i = 0;
var k = 5000; // Character limit for one property (change accordingly)
while (i < currentMessage.length) {
var part = currentMessage.substring(i, i += k);
messageArray.push(part); // Splitting string into an array of strings
}
var userProperties = PropertiesService.getUserProperties();
userProperties.deleteAllProperties(); // Delete old properties
for (var j = 0; j < messageArray.length; j++) { // Set a different property for each element in the array
userProperties.setProperty('messagePart' + j, messageArray[j]);
}
Then, to retrieve the message body, you could first retrieve all the properties corresponding to the message, and then join all the values in a single string with concat. Add the following lines to callAPI:
var userProperties = PropertiesService.getUserProperties();
var keys = userProperties.getKeys(); // Get all property keys
var j = 0;
var currentMessage = "";
do {
var part = userProperties.getProperty('messagePart' + j);
currentMessage = currentMessage.concat(part); // Concat each property value to a single string.
j++;
} while (keys.indexOf('messagePart' + j) !== -1); // Check if property key exists
Reference:
Properties Service
String.prototype.substring()
String.prototype.concat()
I hope this is of any help.

Hmac256 Signature invalid error Google App Script

I am attempting to retrieve trades from a service called 3Commas in Google Apps Script. I've worked with public endpoints before, but this is the first time I've attempted to work with signed endpoints. I'm currently receiving an error that states:
[19-01-09 16:46:24:592 EST] {"error":"signature_invalid","error_description":"Provided signature is invalid"}
I'm guessing this is a formatting issue on my part. I'm using jsSHA to build the HMAC part. I've tried following the example in the docs. But I haven't quite got it yet. Any suggestions on what it could be?
3Commas Docs: https://github.com/3commas-io/3commas-official-api-docs#signed--endpoint-security
function main() {
var key = 'apikey';
var secret = 'apisecret';
var baseUrl = "https://3commas.io/public/api";
var endPoint = "/ver1/smart_trades";
var pointParams = "?limit=10&offset=&account_id=&scope=&type="
//base url + end point + params
var queryString = baseUrl+endPoint+pointParams;
var message = queryString;
var secret = secret;
var shaObj = new jsSHA("SHA-256", "TEXT");
shaObj.setHMACKey(secret, "B64");
shaObj.update(message);
var signature = shaObj.getHMAC("B64");
//headers
var hparams = {
'method': 'get',
'headers': {'APIKEY': key,
'Signature': signature},
'muteHttpExceptions': true
};
//call
var data = UrlFetchApp.fetch(queryString , hparams).getContentText();
Logger.log(data)
}
How about this modification? From 3Commas Docs in your question, I propose the modification points as follows.
Modification points:
It seems that the value which is required to encrypt is after https://3commas.io.
You can encrypt the values using the method of computeHmacSha256Signature() in Class Utilities of GAS. In this case, jsSHA is not required to be used.
But when computeHmacSha256Signature() is used, the value becomes the bytes array of the signed hexadecimal. So it is required to convert it to the unsigned hexadecimal.
Modified script:
function main() {
var key = 'apikey';
var secret = 'apisecret';
var baseUrl = "https://3commas.io"; // Modified
var endPoint = "/public/api/ver1/smart_trades"; // Modified
var pointParams = "?limit=10&offset=&account_id=&scope=&type="; // or "?limit=10"
var queryString = endPoint + pointParams; // Modified
var signature = Utilities.computeHmacSha256Signature(queryString, secret); // Added
signature = signature.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join(""); // Added
//headers
var hparams = {
'method': 'get',
'headers': {'APIKEY': key,
'Signature': signature},
'muteHttpExceptions': true
};
//call
var data = UrlFetchApp.fetch(baseUrl + queryString , hparams).getContentText(); // Modified
Logger.log(data)
}
Note:
About var pointParams = "?limit=10&offset=&account_id=&scope=&type=", in the case of the endpoint you use, limit, offset, account_id, scope and type are no mandatory. So it might be var pointParams = "?limit=10". If the error occurs, please try it.
References:
computeHmacSha256Signature(value, key)
Public Rest API for 3commas.io (2018-10-26)
This document is more detail.
I cannot confirm whether this modified script works. I'm sorry for this situation. So if it didn't work, I apologize. At that time, can you provide the detail information of the situation?

Parsing HTML/XML with XPath in node.js

I'm trying to write an XPath statement to fetch the contents of each row in a table, but only when the 2nd column of each row is not set to "TBA".
The page I am working off this page. I am new to using XPath.
I've come up with the following statement, which I've managed to test successfully (or appears successful anyway) with an online XPath tester, but have been unable to figure out how to apply it in node.js:
//*[#id="body_column_left"]/div[4]/table/tbody/tr/[not(contains(./td[2], 'TBA'))]
This is my attempt below, I've tried variations but I can't get it to even validate as a valid XPath statement and as a result I've been lost in not very helpful stack traces:
var fs = require('fs');
var xpath = require('xpath');
var parse5 = require('parse5');
var xmlser = require('xmlserializer');
var dom = require('xmldom').DOMParser;
var request = require('request');
var getHTML = function (url, callback) {
request(url, function (error, response, body) {
if (!error && response.statusCode == 200) {
return callback(body) // return the HTML
}
})
}
getHTML("http://au.cybergamer.com/pc/csgo/ladder/scheduled/", function (html) {
var parser = new parse5.Parser();
var document = parser.parse(html.toString());
var xhtml = xmlser.serializeToString(document);
var doc = new dom().parseFromString(xhtml);
var select = xpath.useNamespaces({"x": "http://www.w3.org/1999/xhtml"});
var nodes = select("//x:*[#id=\"body_column_left\"]/div[4]/table/tbody/tr/[not(contains(./td[2], 'TBA'))]", doc);
console.log(nodes);
});
Any help would be appreciated!
I ended up solving this issue using cheerioinstead of xpath:
See below:
var $ = cheerio.load(html);
$('.s_grad br').replaceWith("\n");
$('.s_grad thead').remove();
$('.s_grad tr').each(function(i, elem) {
rows[i] = $(this).text();
rows[i] = rows[i].replace(/^\s*[\r\n]/gm, ""); // remove empty newlines
matches.push(new match($(this).find('a').attr('href').substring(7).slice(0, -1))) // create matches
});
How about using this xpath-html, I loved its simplicity.
const xpath = require("xpath-html");
const nodes = xpath
.fromPageSource(html)
.findElements("//img[starts-with(#src, 'https://cloud.shopback.com')]");

What is the best way to split a variable containing a json from an API and then to put it into a specific cell?

Here is my code:
var pUrl = "http://api.perk.com/api/user/id/";
var tk = "/token/";
var options = {
"method" : "GET",
"contentType": 'application/text'
}
function perkTvApi1() {
var response = UrlFetchApp.fetch("http://api.perk.com/api/user/id/515098/token/01133528575d15554742e5bb9bc1fc484fd95ac2/", options);
Logger.log(response.getContentText());
}
I'm trying to figure out how to split the response variable so I can then put it into a spreadsheet row which corresponds to a time stamp.
I haven't been able to find any kind of split function, like I would use in javascript, in Script Services. I'm running out of ideas and approaches.
The easiest way is to use the JSON library: the two most useful functions are JSON.parse() and JSON.stringify().
Both are native to Google Apps Script, so you'd call something like
var object = JSON.parse(response)
in perkTvApi1. Now, the object contains an actual object, which is exactly what you want.
Then, it's just a matter of setting the right cell, so if you wanted cell A1 to be the first name:
SpreadsheetApp.getActiveSheet().getRange('A1').setValue(object["firstName"]);
JSON.parse() is the method you want. Check out this thread
function myFunction() {
var options = {
"method" : "GET",
"contentType": 'application/text'
}
var response = UrlFetchApp.fetch("http://api.perk.com/api/user/id/515098/token/01133528575d15554742e5bb9bc1fc484fd95ac2/", options);
var jsonResponse = JSON.parse(response)
Logger.log(jsonResponse.firstName);
Logger.log("Key length: " + Object.keys(jsonResponse).length);
var keys = Object.keys(jsonResponse);
//Loop through the keys array to push the json object into an array
//The array can then be used to set values in spreadsheet range
//May need to create 2d array depending on how you want your rows/columns arranged.
var ssData = [];
for (var i in keys){
ssData.push(jsonResponse[keys[i]]);
}
Logger.log(ssData);
}