Timed based triggered Twilio SMS from Google Sheets - google-apps-script

I am using the script to send time-based SMS for Date/Time row in a google sheet using Twilio example from this tutorial
https://github.com/jmadden/twilio-sms-for-google-sheets/blob/master/README.md
I have set up everything like on the tutorial and it works, sort off. Instead of the script sending the message at the exact time from the cells using the triggers, it sends an SMS at each trigger execution until the exact time has passed. So if the trigger is set to 5 min it will send an SMS every 5 minutes instead of checking when to send the SMS. Can anybody help with this issue?
// Gets predefined properties for this script. See: File -> Project properties -> Script properties
var prop = PropertiesService.getScriptProperties();
// Returns a specific Google Sheet by URL.
var spreadSheet = SpreadsheetApp.openByUrl(prop.getProperty('spreadsheetUrl'));
// Defines how we want the date to be formatted for scheduling.
var dateFormat = prop.getProperty('DateFormat');
// Returns the specific sheet/tab inside a Google Sheed doc.
var sheet = spreadSheet.getSheets()[0];
// The Row where data starts. This skips the headers row.
var startRow = 2;
// Returns the number of rows with values in this sheet.
var numRows = sheet.getLastRow() - 1;
// Returns all the data to be processed in this sheet. i.e. to # and message body.
var data = sheet.getRange(startRow, 1, numRows, 4).getValues();
// Whenever this function is called it will send an SMS using Twilio
// if all of the required parameters are passed into the function.
function sendSms(to, body) {
// URL used for sending request to Twilio's Messages API. Be sure to include your Account SID
var messages_url = "https://api.twilio.com/2010-04-01/Accounts/"+prop.getProperty('ACCOUNT_SID')+"/Messages.json";
// Parameters needed to send an SMS.
var payload = {
"To": "+"+to,
"Body" : body,
"From" : prop.getProperty('TWNUM')
};
// Contains the method of communicating with the API (POST) and the parameters needed to build a message.
var options = {
"method" : "post",
"payload" : payload
};
// Authorize your account to send this message.
options.headers = {
"Authorization" : "Basic " + Utilities.base64Encode(prop.getProperty('ACCOUNT_SID')+":"+prop.getProperty('AUTH_TOKEN'))
};
UrlFetchApp.fetch(messages_url, options)
}
// This function loops through your Google Sheet and uses the sendSms() function to send messages.
function sendAll() {
// For loop through your Google Sheet's data.
for (i in data) {
var row = data[i];
// Returns the Google Sheet's timezone info as an object.
var when = Moment.moment.tz(data[i][3], dateFormat, spreadSheet.getSpreadsheetTimeZone());
var now = new Date();
// Compares the current time to the "When" time in the sheet.
// Sends SMS if "When" time is older or equal to the current time.
if (isNaN(when) || !when.isValid() || (when.toDate() >= now)){
// Try sending SMS.
try {
response_data = sendSms(row[0], row[1]);
status = "sent";
} catch(err) {
Logger.log(err);
status = "error";
}
sheet.getRange(startRow + Number(i), 3).setValue(status);
}
}
}
// Runs the full script.
function runApp() {
sendAll();
}

I could not make sense of if (isNaN(when) || !when.isValid() || (when.toDate() >= now)) and why it puts the status string in place of the date/time with sheet.getRange(startRow + Number(i), 3).setValue(status); but here is something else you could try.
As there is a status column (the third one) which gets set with "send" after the row is processed, this can be used to prevent running the "send message" code again for the given row.
In the code you posted, replace the sendAll() function with the code below.
function sendAll() {
// For loop through your Google Sheet's data.
for (i in data) {
var row = data[i];
// Returns the Google Sheet's timezone info as an object.
var when = Moment.moment.tz(data[i][3], dateFormat, spreadSheet.getSpreadsheetTimeZone());
var now = new Date();
// Compares the current time to the "When" time in the sheet.
// Sends SMS if current time is older or equal to the time in the sheet.
if ((data[i][2] != "sent") && (now >= when.toDate())) {
// Try sending SMS.
try {
response_data = sendSms(row[0], row[1]);
status = "sent";
} catch (err) {
Logger.log(err);
status = "error";
}
sheet.getRange(startRow + Number(i), 3).setValue(status);
}
}
}

Related

How to return google sheet values using doPost using x-www-form-urlencoded?

I try to use google sheets to write and read some data using post requests,
the writing part works, but it never returns any value back.
function doPost(e) { return handleResponse(e); }
function handleResponse(e) {
// Get public lock, one that locks for all invocations
// (https://gsuite-developers.googleblog.com/2011/10/concurrency-and-google-apps-script.html)
var lock = LockService.getPublicLock();
// Allow the write process up to 2 seconds
lock.waitLock(2000);
try {
// Generate a (not very good) UUID for this submission
var submissionID = e.parameter.id || 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
// Open the spreadsheet document and select the right sheet page
var sheetName = e.parameter.sheet_name|| 'Sheet1';
var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("key"));
var sheet = doc.getSheetByName(sheetName);
//get information out of post request
var action = e.parameter.action || 'save';
var pName = e.parameter.name;
var rowNumber = findRow(pName,sheetName);
var headRow = e.parameter.header_row || 1;
var headers = sheet.getRange(headRow, 1, 1, sheet.getLastColumn()).getValues()[0];
// check for action is loading
if(action == 'load'){
//check if the name has data
if (rowNumber){
//loads all the give values out of the parameters
var answer = [];
Logger.log('hadders: ' + headers);
for (i in headers) {
if (e.parameter[headers[i].toLowerCase()] !== undefined) {
var val = sheet.getRange(rowNumber, 1, 1,sheet.getLastColumn()).getValues()[0][i];
answer.push(val);
}
}
Logger.log('answer: '+ answer);
// Return result in JSON
return ContentService
.createTextOutput({body:{parameter:{answer}}})
.setMimeType(ContentService.MimeType.JSON)
;
}else{
// return error name wasn't found in sheet.
return ContentService
.createTextOutput("can't find Name")
.setMimeType(ContentService.MimeType.TEXT)
;
}
}
The logger returns all the right values,
but logging the return value from this function ends up in an empty object.
I tried just making my own return object like:
return ContentService
.createTextOutput({body={parameter={answer=JSON.stringify(answer)}}})
.setMimeType(ContentService.MimeType.TEXT)
;
I know that I need to use &= instead of ,: but it still returned nothing.
In your script, how about modifying as follows?
From:
return ContentService
.createTextOutput({body:{parameter:{answer}}})
.setMimeType(ContentService.MimeType.JSON)
To:
return ContentService
.createTextOutput(JSON.stringify({body:{parameter:{answer}}}))
.setMimeType(ContentService.MimeType.JSON)
In the case of createTextOutput({body:{parameter:{answer}}}), the object cannot be directly put. So I thought that it is required to convert it to the string.
Note:
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful this.
You can see the detail of this in the report of "Redeploying Web Apps without Changing URL of Web Apps for new IDE".

Posting to Google Sheet is giving {"result":"error","error":{"name":"Exception"}}

I am trying to post to a google sheet using an html form, I followed a tutorial here, but I keep getting an error {"result":"error","error":{"name":"Exception"}} and the sheet is not being updated.
Here is code.gs
// original from: http://mashe.hawksey.info/2014/07/google-sheets-as-a-database-insert-with-apps-script-using-postget-methods-with-ajax-example/
// original gist: https://gist.github.com/willpatera/ee41ae374d3c9839c2d6
/**
* #OnlyCurrentDoc
*/
function doGet(e){
console.log("running");
return handleResponse(e);
}
// Usage
// 1. Enter sheet name where data is to be written below
var SHEET_NAME = "Sheet1";
// 2. Run > setup
//
// 3. Publish > Deploy as web app
// - enter Project Version name and click 'Save New Version'
// - set security level and enable service (most likely execute as 'me' and access 'anyone, even anonymously)
//
// 4. Copy the 'Current web app URL' and post this in your form/script action
//
// 5. Insert column names on your destination sheet matching the parameter names of the data you are passing in (exactly matching case)
var SCRIPT_PROP = PropertiesService.getScriptProperties(); // new property service
// If you don't want to expose either GET or POST methods you can comment out the appropriate function
function doPost(e){
console.log("running2");
return handleResponse(e);
}
function handleResponse(e) {
console.log("running3");
// shortly after my original solution Google announced the LockService[1]
// this prevents concurrent access overwritting data
// [1] http://googleappsdeveloper.blogspot.co.uk/2011/10/concurrency-and-google-apps-script.html
// we want a public lock, one that locks for all invocations
var lock = LockService.getPublicLock();
lock.waitLock(30000); // wait 30 seconds before conceding defeat.
try {
// next set where we write the data - you could write to multiple/alternate destinations
console.log("running4");
var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("key"));
console.log("running5");
var sheet = doc.getSheetByName(SHEET_NAME);
// we'll assume header is in row 1 but you can override with header_row in GET/POST data
var headRow = e.parameter.header_row || 1;
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var nextRow = sheet.getLastRow()+1; // get next row
var row = [];
// loop through the header columns
for (i in headers){
if (headers[i] == "Timestamp"){ // special case if you include a 'Timestamp' column
row.push(new Date());
} else { // else use header name to get data
row.push(e.parameter[headers[i]]);
//row.push("Test")
}
}
// more efficient to set values as [][] array than individually
sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);
// return json success results
return ContentService
.createTextOutput(JSON.stringify({"result":"success", "row": nextRow}))
.setMimeType(ContentService.MimeType.JSON);
} catch(e){
// if error return this
return ContentService
.createTextOutput(JSON.stringify({"result":"error", "error": e}))
.setMimeType(ContentService.MimeType.JSON);
} finally { //release lock
lock.releaseLock();
}
}
function setup() {
var doc = SpreadsheetApp.getActiveSpreadsheet();
SCRIPT_PROP.setProperty("key", doc.getId());
}
I put in the console.logs to see where the issue was and it seems to have to with the line var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("key")); as "running4" is logged, but "running5" is not.
Figured it out, it was a permissions error caused by the
/**
* #OnlyCurrentDoc
*/
In the script. I had added that because I keep getting a security error when trying to give my script access, but I solved that with this answer.

This script does not populate sheet after parsing retrieved data

I hope this is well explained. First of all, sorry because my coding background is zero and I am just trying to "fix" a previously written script.
Problem The script does not populate sheet after parsing retrieved data if the function is triggered by timer and the sheet is not open in my browser .
The script works OK if run it manually while sheet is open.
Problem details:
When I open the sheet the cells are stuck showing "Loading" and after a short time, data is written.
Expected behavior is to get the data written no matter if I don't open the sheet.
Additional info: This is how I manually run the function
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [
{name: "Manual Push Report", functionName: "runTool"}
];
sheet.addMenu("PageSpeed Menu", entries);
}
Additional info: I set the triggers with Google Apps Script GUI See the trigger
Before posting the script code, you can see how the cells look in the sheet:
Script code
function runTool() {
var activeSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Results");
var rows = activeSheet.getLastRow();
for(var i=3; i <= rows; i++){
var workingCell = activeSheet.getRange(i, 2).getValue();
var stuff = "=runCheck"
if(workingCell != ""){
activeSheet.getRange(i, 3).setFormulaR1C1(stuff + "(R[0]C[-1])");
}
}
}
// URL check //
function runCheck(Url) {
var key = "XXXX Google PageSpeed API Key";
var strategy = "desktop"
var serviceUrl = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=" + Url + "&key=" + key + "&strategy=" + strategy +"";
var array = [];
var response = UrlFetchApp.fetch(serviceUrl);
if (response.getResponseCode() == 200) {
var content = JSON.parse(response.getContentText());
if ((content != null) && (content["lighthouseResult"] != null)) {
if (content["captchaResult"]) {
var score = content["lighthouseResult"]["categories"]["performance"]["score"];
} else {
var score = "An error occured";
}
}
array.push([score,"complete"]);
Utilities.sleep(1000);
return array;
}
}
You can try the code using the sheet below with a valid Pagespeed API key.
You only need to add a Trigger and wait for it's execution while the sheet is not open in your browser
https://docs.google.com/spreadsheets/d/1ED2u3bKpS0vaJdlCwsLOrZTp5U0_T8nZkmFHVluNvKY/copy
I suggest you to change your algorithm. Instead of using a custom function to call UrlFetchApp, do that call in the function called by a time-driven trigger.
You could keep your runCheck as is, just replace
activeSheet.getRange(i, 3).setFormulaR1C1(stuff + "(R[0]C[-1])");
by
activeSheet.getRange(i, 3, 1, 2).setValues(runCheck(url));
NOTE
Custom functions are calculated when the spreadsheet is opened and when its arguments changes while the spreadsheet is open.
Related
Cache custom function result between spreadsheet opens

Find & Replace - Google Sheets Script after GetData

I'm having some trouble with my script. Basically, that works getting data from one course and inputting the values to one Sheet. That's working perfectly. But when one of my students input 'enter' command at that course, I have trouble to read it in Excel.
SO, I have to find (it's a regular Expression: \r\n|\n|\r) and replace the enter at Google Spreadsheet and change it for "; ". Works perfectly doing it manually, but I can't do it by script. Here the piece:
// 1. Enter sheet name where data is to be written below
var SHEET_NAME = "DATA";
// 2. Run > setup
//
// 3. Publish > Deploy as web app
// - enter Project Version name and click 'Save New Version'
// - set security level and enable service (most likely execute as 'me' and access 'anyone, even anonymously)
//
// 4. Copy the 'Current web app URL' and post this in your form/script action
//
// 5. Insert column names on your destination sheet matching the parameter names of the data you are passing in (exactly matching case)
var SCRIPT_PROP = PropertiesService.getScriptProperties(); // new property service
// If you don't want to expose either GET or POST methods you can comment out the appropriate function
function doGet(e){
return handleResponse(e);
}
function doPost(e){
return handleResponse(e);
}
function handleResponse(e) {
// shortly after my original solution Google announced the LockService[1]
// this prevents concurrent access overwritting data
// [1] http://googleappsdeveloper.blogspot.co.uk/2011/10/concurrency-and-google-apps-script.html
// we want a public lock, one that locks for all invocations
var lock = LockService.getPublicLock();
lock.waitLock(30000); // wait 30 seconds before conceding defeat.
try {
// next set where we write the data - you could write to multiple/alternate destinations
var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("CHANGED BY SECURITY REASON"));
var sheet = doc.getSheetByName(SHEET_NAME);
// we'll assume header is in row 1 but you can override with header_row in GET/POST data
var headRow = e.parameter.header_row || 1;
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var nextRow = sheet.getLastRow()+1; // get next row
var row = [];
// loop through the header columns
for (i in headers){
if (headers[i] == "Timestamp"){ // special case if you include a 'Timestamp' column
row.push(new Date());
} else { // else use header name to get data
row.push(e.parameter[headers[i]]);
}
}
// more efficient to set values as [][] array than individually
sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);
// return json success results
return ContentService
.createTextOutput(JSON.stringify({"result":"success", "row": nextRow}))
.setMimeType(ContentService.MimeType.JSON);
} catch(e){
// if error return this
return ContentService
.createTextOutput(JSON.stringify({"result":"error", "error": e}))
.setMimeType(ContentService.MimeType.JSON);
} finally { //release lock
lock.releaseLock();
}
}
function setup() {
var doc = SpreadsheetApp.getActiveSpreadsheet();
SCRIPT_PROP.setProperty("1Un5A61M8CJDBGDAB-Tx-lYgKYaVB2RSfn9QAQ5Q-sZs", doc.getId());
}
I tried this and worked well, but needs a trigger that's play only minute per minute. Not ok.. And change just the first value from the cell. Sometimes there's more than one at same cell:
function Replace() {
var redeletar = new RegExp("\r\n|\n|\r");
var range = SpreadsheetApp.getActiveSheet()
.getRange('A1:CH');
range.setValues(range.getValues()
.map(function (r) {
return r.map(function (c) {
//Replace string
return c.toString().replace(redeletar, ";");
});
}));
}
Any idea to play this script at same time after the Sheet receive de data from my course AND play it recursively?
Thanks so mucho
I think all you need is the global flag as shown below:
function Replace()
{
var re = /(\r\n|\n|\r")/g;
var range = SpreadsheetApp.getActiveSheet().getActiveRange();
range.setValues(range.getValues().map(function (r){return r.map(function (c){return c.toString().replace(re, ";");});}));
}
I think this will make all of the replacements rather than just the first ones;

How to pass parameters from one Google-Apps-Script to another and execute?

Goal is to pass data from Google Apps Script A to Google Apps Script
B.
Script A is published with execute as user permissions.
Script B is published with execute as me permissions (owner).
At the very least I want to be able to pass Session.getActiveUser.getEmail() from script A to script B.
This is what I have so far...
Script A
// Script-as-app template.
function doGet() {
var app = UiApp.createApplication();
var button = app.createButton('Click Me');
app.add(button);
var handler = app.createServerHandler('myClickHandler');
button.addClickHandler(handler);
return app;
}
function myClickHandler(e) {
var url = "https://script.google.com/macros/s/AKfycbzSD3eh_SDnbA4a7VCkctHoMGK8d94SAPV2IURR3pK7_MwLXIb4/exec";
var payload = {
name : "Gene",
activeUser : Session.getActiveUser().getEmail(),
time : new Date()
};
var params = {
method : "post",
payload : payload
}
Logger.log("Hello World!");
var HTTPResponse;
try{
HTTPResponse = UrlFetchApp.fetch(url, params);
}catch(e){
Logger.log(e);
}
return HTTPResponse;
}
Script B
function doPost(e){
if(typeof e === 'undefined')
return;
var app = UiApp.createApplication();
var panel = app.add(app.createVerticalPanel());
for(var i in e.parameter){
panel.add(app.createLabel(i + ' : ' + e.parameter[i]));
Logger.log(i + ' : ' + e.parameter[i]);
}
ScriptProperties.setProperty('Donkey', 'Kong');
return app;
}
output
Going to script A here the page loads the button. Clicking the button causes "Hello World!" to be logged in Script A's project log but the log of Script B's project remains empty.
TryCatch does not log any error.
I believe your problem is due that you try to pass as a response argument a uiapp element.
here a little variation of your script in html service.
the demo
the script:
// #### Part A
function doGet(e) {
var html ="<input type='text' id='text' /><input type='button' onclick='myClick()' value='submit'>"; // a text to be passed to script B
html+="<div id='output'></div>"; // a place to display script B answer
html+="<script>";
html+="function myClick(){google.script.run.withSuccessHandler(showResults).myClickHandler(document.getElementById('text').value);}"; // handler to do the job in script A
html+="function showResults(result){document.getElementById('output').innerHTML = result;}</script>"; // function to show the result of the urlfetch (response of script B)
return HtmlService.createHtmlOutput(html);
}
function myClickHandler(text) {
var url = ScriptApp.getService().getUrl();
var payload = {
name : "Gene",
text : text,
time : new Date()
};
var params = {
method : "post",
payload : payload
}
Logger.log("text: "+text);
var HTTPResponse;
try{
HTTPResponse = UrlFetchApp.fetch(url, params);
}catch(e){
Logger.log(e);
}
return HTTPResponse.getContentText();
}
// ###### Part B
function doPost(e){
if(typeof e === 'undefined'){
return "e was empty!!";
}
var htmlOut="<ul>";
for(var i in e.parameter){
htmlOut+="<li>"+i+ " : " + e.parameter[i]+"</li>";
if(i=="text"){
htmlOut+="<li> Text hash : "+Utilities.base64Encode(Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, e.parameter[i]))+"</li>";
}
}
htmlOut+="</ul>";
return ContentService.createTextOutput(htmlOut);
}
It is important to note that you won't have the ability to get the logger events of the script B (because it is triggered when you are not there - you are not the one who trigger script B. this is script A that's trigger script B and script A is not identified as "you" when it make a urlfetch). If you want to get the result of the script b logger you should return it to the script a.
It's important to note: again, when script A do the UrlFetch to script B it is not identified as "you" so the script B must accept to be opened by anyone (in the publish option under "Who has access to the app:" you need to select anyone even anonymous).
NB: i put everything in the same script for commodity (you can split that in two differents script it's not a problem) and because B part need to be accessed to anonymous persons I can't retrieve automatically the email adress in the part A so I changed a little bit what was done here.