In rCharts, one can set JS callbacks of DataTables using a special string notation: #! function(par) {...} !#. For example, let's look into the following R code:
#JS callback to truncate long strings in table cells and add a tooltip
callback = "#!
function (nRow) {
$('td', nRow).each(function (index) {
var maxChars = 80;
var unfilteredText = $(this).text();
if (unfilteredText.length > maxChars && maxChars > 3) {
$(this).attr('title', unfilteredText);
$(this).html(unfilteredText.substring(0, maxChars-4) + '...');
}
});
return nRow;
} !#"
result <- dTable(df, aaSorting = list(c(5, "desc")), sPaginationType="full_numbers",
fnRowCallback=callback)
Is this possible in Shiny DataTables?
I just stumbled across this question and was able to use the information here, along with help from section 4.5 of this post to solve this problem. In order to get this to work, you would simply do the following:
library(DT)
long_strings = replicate(10, paste(sample(c(0:9, letters, LETTERS), 135, replace = TRUE), collapse = ""))
dat <- data.frame(x = 1:10,
y = month.abb[1:10],
z = long_strings,
stringsAsFactors = FALSE)
DT::datatable(dat,
options = list(
rowCallback = JS("function(row, data) {",
" var max_chars = 80, full_text = data[3];",
" if (full_text.length > max_chars) {",
" $('td:eq(3)', row).attr('title', full_text);",
" $('td:eq(3)', row).html(full_text.substring(0, max_chars - 4) + '...');",
" }",
"}")))
It's important to note that since we want this to operate on a per row basis, we use the rowCallback function inside of the options parameter. This is in contrast to the callback function that can be used on the entire table, which is its own parameter for DT::datatable.
Also take note of the fact that you don't have to call .text() or .innerHTML or anything of that sort on data[3]. The value that is returned is the text value of that cell.
Hopefully someone in the future stumbles across this and finds it beneficial.
Related
I tried applying this solution to my case:
Emailing SPARKLINE charts sends blank cells instead of data
But when I try to apply it to my situation an error pops up with:
TypeError: Cannot read property '0' of null
On the executions there is more information about this error:
My GAS code for my Email solution is able to send just the values, and it's here:
function alertDailyInfo() {
let emailAddress = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SANDBOX").getRange("F1").getValue();
let treeIconUrl = "https://d1nhio0ox7pgb.cloudfront.net/_img/g_collection_png/standard/256x256/tree.png";
let treeIconBlob = UrlFetchApp
.fetch(treeIconUrl)
.getBlob()
.setName("treeIconBlob");
let treeUpdate = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SANDBOX").getRange("F6").getValue();
let waterUpdate = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SANDBOX").getRange("F11").getValue();
if (treeUpdate > 0) {
MailApp.sendEmail({
to: emailAddress,
subject: "TREE WATER UPDATE",
htmlBody: "<img src='cid:treeIcon'><br>" + '<br>' + '<br>' +
'<b><u>Tree average is:</u></b>'+ '<br>' + treeUpdate + '<br>' + '<br>' +
'<b><u>Water average is:</u></b>'+ '<br>' + waterUpdate + '<br>' + '<br>'
,
inlineImages:
{
treeIcon: treeIconBlob,
}
});
}
}
The code from the solution presented on the link above and which I have tried to adapt to my situation (please check my file below) is here:
drawTable();
function drawTable() {
let emailAddress1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SANDBOX").getRange("F1").getValue();
var ss_data = getData();
var data = ss_data[0];
var background = ss_data[1];
var fontColor = ss_data[2];
var fontStyles = ss_data[3];
var fontWeight = ss_data[4];
var fontSize = ss_data[5];
var html = "<table border='1'>";
var images = {}; // Added
for (var i = 0; i < data.length; i++) {
html += "<tr>"
for (var j = 0; j < data[i].length; j++) {
if (typeof data[i][j] == "object") { // Added
html += "<td style='height:20px;background:" + background[i][j] + ";color:" + fontColor[i][j] + ";font-style:" + fontStyles[i][j] + ";font-weight:" + fontWeight[i][j] + ";font-size:" + (fontSize[i][j] + 6) + "px;'><img src='cid:img" + i + "'></td>"; // Added
images["img" + i] = data[i][j]; // Added
} else {
html += "<td style='height:20px;background:" + background[i][j] + ";color:" + fontColor[i][j] + ";font-style:" + fontStyles[i][j] + ";font-weight:" + fontWeight[i][j] + ";font-size:" + (fontSize[i][j] + 6) + "px;'>" + data[i][j] + "</td>";
}
}
html += "</tr>";
}
html + "</table>"
MailApp.sendEmail({
to: emailAddress1,
subject: "Spreadsheet Data",
htmlBody: html,
inlineImages: images // Added
})
}
function getData(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SANDBOX");
var ss = sheet.getDataRange();
var val = ss.getDisplayValues();
var background = ss.getBackgrounds();
var fontColor = ss.getFontColors();
var fontStyles = ss.getFontStyles();
var fontWeight = ss.getFontWeights();
var fontSize = ss.getFontSizes();
var formulas = ss.getFormulas(); // Added
val = val.map(function(e, i){return e.map(function(f, j){return f ? f : getSPARKLINE(sheet, formulas[i][j])})}); // Added
return [val,background,fontColor,fontStyles,fontWeight,fontSize];
}
// Added
function getSPARKLINE(sheet, formula) {
formula = formula.toUpperCase();
if (~formula.indexOf("SPARKLINE")) {
var chart = sheet.newChart()
.setChartType(Charts.ChartType.SPARKLINE)
.addRange(sheet.getRange(formula.match(/\w+:\w+/)[0]))
.setTransposeRowsAndColumns(true)
.setOption("showAxisLines", false)
.setOption("showValueLabels", false)
.setOption("width", 200)
.setOption("height", 100)
.setPosition(1, 1, 0, 0)
.build();
sheet.insertChart(chart);
var createdChart = sheet.getCharts()[0];
var blob = createdChart.getAs('image/png');
sheet.removeChart(createdChart);
return blob;
}
}
The code that is working just for the values, which I pasted above (1st block of code), will send me an email like this:
But I need to receive the email like this, with the Sparklines below the values like so:
The code for the Email solution, just for the values, I pasted above (1st block of code) is working. But for some reason when the code from the solution linked above (2nd block of code) is imported/saved into my Google Sheets file GAS script library and adapted to my case, everything stops working, displaying the errors mentioned above.
So basically, as you might have already understood, I need to send emails with the values from Tree Average and Water Average, and I managed to get that working. But I also need for the Sparkline graphs that you can see below, and by checking my file linked below too, to also be sent as images/blobs, just below the info, like in the screenshot above.
Can anyone provide any pointers on what can be missing in applying the solution above or is there a better alternative to sending a SPARKLINE graph as image/blob by email?
Here is my file:
https://docs.google.com/spreadsheets/d/1ExXtmQ8nyuV1o_UtabVJ-TifIbORItFMWjtN6ZlruWc/edit?usp=sharing
EDIT_1:
I made some edits to bring more clarity.
EDIT_2:
As requested this is the formula applied to the first Sparkline, the 2nd one is pretty much the same:
=ARRAYFORMULA( SPARKLINE(QUERY({IFERROR(DATEVALUE(SANDBOX!$A$2:$A)), SANDBOX!$B$2:$B},
"select Col2
where Col2 is not null
and Col1 <= "&INT(MAX(SANDBOX!$A$2:$A))&"
and Col1 > "&INT(MAX(SANDBOX!$A$2:$A))-(
IFERROR(
VLOOKUP(
SUBSTITUTE($F$4," ",""),
{"24HOURS",0;
"2DAYS",1;
"3DAYS",4;
"7DAYS",8;
"2WEEKS",16;
"1MONTH",30;
"3MONTHS",90;
"6MONTHS",180;
"1YEAR",365;
"2YEARS",730;
"3YEARS",1095},
2,FALSE))
)-1, 0),
{"charttype","column";"color","#00bb21";"empty","ignore";"nan","ignore"}))
EDIT_3: At the advice of Rubén I have removed drawTable(); at the beggining of the code block.
I have also transfered the formula for the Sparkline to another helper sheet and link it to the main sheet.
After trying it seems the error does not appear anymore. Although the email received has 2 problems:
I receive the whole sheet in table form, where I just wanted the Sparklines.
Also the Sparklines do not come as images, they do not show up at all. Also where they should appear it says undefined.
I guess the whole sheet is being set because the function getting the range getDataRange(); is getting the whole sheet range.
Here is a screenshot:
As the question you reference explains:
the chart created by SPARKLINE cannot be directly imported to the email.
Why isn't the script working? Because you have not made any significant modifications to it and because you are using a more complex formula than the one proposed in the other question, it is very difficult (if not impossible) to make it work without any modifications.
What are the options? In my opinion you have 3 different options.
Follow the logic of the solution proposed by Tanaike in the other question and using EmbeddedChartBuilder try to shred the content of the FORMULA to achieve the same as with SPARKLINE.
Use the SpreadsheetApp methods to directly get the values from the sheet and build the chart from there.Here is a small example of how you can do it using Chart Service (You could achieve exactly the same with EmbeddedChartBuilder). As you already have a Blob object, you can insert it inside an email as I do inside the Sheet.
function constCreateChart() {
const sS = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('HELPER')
const chart = Charts.newDataTable()
.addColumn(Charts.ColumnType.NUMBER, '')
.addColumn(Charts.ColumnType.NUMBER, '')
// Modfify with your data
// getRange('A2:A15').getValues()...
const builder = [...Array(100).keys()].forEach(n => {
chart.addRow([n, n * n * Math.random()])
})
chart.build()
const chartShap = Charts.newColumnChart()
.setDataTable(chart)
.setLegendPosition(Charts.Position.NONE)
.setOption('hAxis.ticks', [])
.setOption('vAxis.ticks', [])
.build()
sS.insertImage(chartShap.getAs('image/png'), 5, 5)
}
Result
Use this form to request Google to add the possibility to convert charts obtained using SPARKLINES to Blob objects that can be used inside an email.
Documentation
Avalible Options in Chart Service
Fundamentals of Apps Script with Google Sheets #5:Chart and Present Data in Slides
Remove drawTable(); as this line makes that the drawTable function be executed when any function be called.
Apparently the error occurs on .addRange(sheet.getRange(formula.match(/\w+:\w+/)[0])), more specifically because formula.match(/\w+:\w+/) (this expression is intended to extract a range reference of the form A1:B10) returns null. Unfortunately the question doesn't include the formula. One possible solution might be as simple as replacing sheet.getRange(formula.match(/\w+:\w+/)[0]) by another way to set the source range for the temporary chart, but might be a more complex, i.e. adding a helper sheet to be used as the data source for the temporary chart.
NOTE: On Rev 11 one in-cell sparklines chart formula was added. As the formula is pretty complex, the simplest solution is to add a helper sheet to add the QUERY function
QUERY({IFERROR(DATEVALUE(SANDBOX!$A$2:$A)), SANDBOX!$B$2:$B},
"select Col2
where Col2 is not null
and Col1 <= "&INT(MAX(SANDBOX!$A$2:$A))&"
and Col1 > "&INT(MAX(SANDBOX!$A$2:$A))-(
IFERROR(
VLOOKUP(
SUBSTITUTE($F$4," ",""),
{"24HOURS",0;
"2DAYS",1;
"3DAYS",4;
"7DAYS",8;
"2WEEKS",16;
"1MONTH",30;
"3MONTHS",90;
"6MONTHS",180;
"1YEAR",365;
"2YEARS",730;
"3YEARS",1095},
2,FALSE))
)-1, 0)
Then instead of sheet.getRange(formula.match(/\w+:\w+/)[0]) use helperSheet.getDataRange(). You will have to set an appropriate way to declare helperSheet.
Related to Rev. 8
The code on Tanaike's answer reads data from Sheet1 but your sheet is named SANDBOX.
I'm translating VBA into a Google Sheets script, but am a loss on how to get past this loop through all rows in a single column hurdle.
I have a column which contains an #OrdernNumber, a -State code, a First name, and sometimes a Last name, followed by an empty cell. I need to combine the First and Last names into the First name cell, and then clear the Last name cell below. This is illustrated in the screenshot below:
Column C and D in the screenshot reveal the pattern I used to construct my original VBA code:
Sub WorkingCombineAndClearLoop()
Dim Rngcell As Range
For Each Rngcell In Range("B1:B100")
'if first character is #
If left(Rngcell.Value, 1) <> "#" _
'and if first character is -
And left(Rngcell.Value, 1) <> "-" _
'and if cell is not blank then
And Rngcell.Value <> "" Then
'combine left cell
Rngcell.Value = Rngcell.Offset(0, -1).Value _
'with bottom left cell
& " " & Rngcell.Offset(1, -1).Value
'then clear below cell
Rngcell.Offset(1, 0).ClearContents
End If
Next
End Sub
Following is my annotated JavaScript code. Because I can't get the loop working yet, I've had to change the code logic a bit:
function workingCombineAndClearNoLoop() {
//get active spreadsheet, sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
//determine row count
var numRows = SpreadsheetApp.getActiveSheet().getRange("A:A").getLastRow();
//if A1's first character is not # AND
if (s.getRange('A1').getValue().substring(0,1) === "#" &&
//if A2's first character is not ' AND
s.getRange('A1').offset(1, 0).getValue().substring(0,1) === "-" ) {
//Set the value of A2 to...
s.getRange('A1').offset(2, 1).setValue(
//A2 cell content + " " + A2 cell content (concatenate)
s.getRange('A1').offset(2, 1).getValue() + " " +
s.getRange('A1').offset(3, 1).getValue())
//and then clear A2
s.getRange('A1').offset(3, 1).clearContent()
};
}
The following is where everything falls apart - the loop. I've only left the bare bones in this final copy of the code:
function notWorkingCombineAndClearWithLoop() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var numRows = SpreadsheetApp.getActiveSheet().getRange("A:A").getLastRow();
//loop
//how to replace 'A1' with A[i]?, so that it loops through all cells in the column?
for (var i = 0; i < 25; i++) {
if (s.getRange('A1').getValue().substring(0,1) === "#" &&
s.getRange('A1').offset(1, 0).getValue().substring(0,1) === "-" ) {
s.getRange('A1').offset(2, 1).setValue(
s.getRange('A1').offset(2, 1).getValue() + " " +
s.getRange('A1').offset(3, 1).getValue())
s.getRange('A1').offset(3, 1).clearContent()
}
};
}
Note: I've completed Codacademy's JS course and several Google Sheets tutorials, and am pretty good at VBA, but I'm still swimming in circles with this. I've tried to follow many loop examples, using variables in place of A1. Because I can't find any working solution, I've left the non-functioning A1 as a simple placeholder.
Short answer
//how to replace 'A1' with A[i]?, so that it loops through all cells
//in the column?
Use
'A' + i
Explanation
getRange() has several forms, one of them use A1 notation reference. This is used in concordance with the Google Apps Script included in the question.
Example
The main intention of below code is to show how to use the A1 notation to loop through the cells of a column in Google Apps Script.
There was some errors in the indexes. They were corrected.
Also changed the fourth line to reduce the rows to loop through and added a couple of lines.
function editedNotWorkingCombineAndClearWithLoop() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var numRows = SpreadsheetApp.getActiveSheet().getLastRow();
/*
* use variables to reduce number of calls to Apps Script services
* and improve readability
*/
var range, substring1, substring2;
//loop
for (var i = 1; i <= numRows; i++) {
//Here is the magic
range = s.getRange('A' + i );
substring1 = range.getValue().substring(0,1);
substring2 = range.offset(1, 0).getValue().substring(0,1);
if ( substring1 === '#' && substring2 === '-' ) {
range.offset(0, 1).setValue(range.getValue());
range.offset(1, 1).setValue(range.offset(1, 0).getValue());
range.offset(2, 1).setValue(
range.offset(2, 0).getValue()
+ " "
+ range.offset(3, 0).getValue()
)
}
}
}
I'm trying to make a datatable object that has a child table with each row. I'm following the format of a Child Rows example given by Yihui Xie.
The example shows how to pass a simple string in, and I can even do rudimentary tables. But I don't want to hardcode a lot of HTML, since R has great facilities to write HTML for me.
My problem is that if I user R to generate an character string as HTML, when I pass it into the datatable object through JS, it reads it as literal text instead of rendering it as HTML.
I'm sure it's a simple solution, but using htmltools::htmlPreserve and htmltools::HTML don't change anything, and I suspect I'll have to do something within the javascript.
DT::datatable({
mtcars$rows <-
paste0("<tr><td>Gas Mileage</td><td>", mtcars$mpg, "</td></tr>",
"<tr><td>Quarter Mile</td><td>", mtcars$qsec, "</td></tr>")
cbind(' ' = '⊕', mtcars)
},
escape = c(-2, -13),
selection = "single",
options = list(
columnDefs = list(
list(visible = FALSE, targets = c(0, 13)),
list(orderable = FALSE, className = 'details-control', targets = 1)
)
),
callback = JS("
table.column(1).nodes().to$().css({cursor: 'pointer'});
var format = function(d) {
return '<table>' +
'<tr><td>Variable</td><td>Value</td></tr>' +
d[13] +
'</table>';
};
table.on('click', 'td.details-control', function() {
var td = $(this), row = table.row(td.closest('tr'));
if (row.child.isShown()) {
row.child.hide();
td.html('⊕');
} else {
row.child(format(row.data())).show();
td.html('⊖');
}
});"
)
)
Try replacing escape = c(-2, -13) with escape = FALSE.
Looking at the source code, the escape variable is passed into the escapeData function of datatables which looks like this. Hopefully it'll give you a bit of guidance as to how to correctly custom-specify what should/shouldn't be escaped. :)
# `i` here is your `escape` variable which can be either TRUE/FALSE,
# indices, or colnames.
escapeData = function(data, i, colnames) {
if (is.null(data) || prod(dim(data)) == 0 || identical(i, FALSE)) return(data)
// see below for definition of convertIdx
i = convertIdx(i, colnames, ncol(data))
# only escape character columns (no need to escape numeric or logical columns)
data[i] = lapply(data[i], function(x) {
if (is.character(x) || is.factor(x)) htmlEscape(x) else x
})
data
}
# convertIdx looks like this:
# convert character indices to numeric
convertIdx = function(i, names, n = length(names), invert = FALSE) {
if (!is.character(i)) return({
if (invert) {
if (is.numeric(i)) -i else if (is.logical(i)) !i else {
stop('Indices must be either character, numeric, or logical')
}
} else i
})
if (is.null(names)) stop('The data must have column names')
o = setNames(seq_len(n), names)
i = o[i]
if (any(is.na(i)))
stop("Some column names in the 'escape' argument not found in data")
if (invert) o[-i] else i
}
Right now I am getting a select option's value (literal value attribute) from JQuery, the problem is that it takes the first value it finds that simply "contains" that keyword.
For example, if I had a select with two options: Silver Division (value=1) and Silver (value=2), if I called the following line of code it would return (value=1) rather than (value=2)
var ranking = "Silver"; // hard-coded for example
var setIndex = $("#userElo" +" > option:contains(" + ranking + ")").val();
Q: I have been trying to search, with no success, for something like option:equals so that it only looks for exact string matches. I have tried various test-and-guess things like the following.
var setIndex = $("#userElo" + ID + " > option:contains" + ranking).val();
var setIndex = $("#userElo" + ID + " > option[text=" + ranking + "]").val();
var setIndex = $("#userElo" + ID + " > option[text=" + ranking).val();
Here is a JSFiddle Demo on a simple scale showing the Silver Division and Silver issue.
I am running out of ideas though, so if anyone know of some syntax for this solution that'd be awesome!
Thanks!
You'll need to select all of the options that may potentially match, then use the filter() function to narrow them down to those that do actually match the text you want. This should do it:
var ranking = "Silver";
var setIndex = $("#userElo > option").filter(function() {
return $(this).text() === ranking;
}).val();
I was interested in writing a twitter bot to help out some friends at a local ski resort. I found this tutorial from Amit Agarwal which gave me enough to get started (it did take me more than 5 minutes since I did a lot of modifying). I host the script on google docs.
FIRST I think this is javascript (my understanding is that google apps script uses javascript...) and when I have had problems with the code so far, google searches for javascript-such-and-such have been helpful, but if this is not actually javascript, please let me know so I can update the tag accordingly!
I have no prior experience with javascript, so I am pretty happy that it's actually working. But I want to see if I'm doing this right.
The start function initiates the trigger, which kicks off the fetchTweets() function every interval (30 minutes). In order to avoid duplicates (the first errors I encountered) & potentially being flagged as spam, I needed a way to ensure that I was not posting the same tweets over and over again. Within the start() function, the initial since_id value is assigned:
ScriptProperties.setProperty("SINCE_TWITTER_ID", "404251049889759234");
Within the fetchTweet() function, I think I am updating this property with the statement:
ScriptProperties.setProperty("SINCE_TWITTER_ID", lastID + '\n');
Is this a good way to do this? Or is there a better/more reliable way? And if so, how can I be sure it's updating the property? (I can check the log file and it seems to be doing it, so I probably just need to create a permanent text file for the logger).
Any help is greatly appreciated!!
/** A S I M P L E T W I T T E R B O T **/
/** ======================================= **/
/** Written by Amit Agarwal #labnol on 03/08/2013 **/
/** Modified by David Zemens #agnarchy on 11/21/2013 **/
/** Tutorial link: http://www.labnol.org/?p=27902 **/
/** Live demo at http://twitter.com/DearAssistant **/
/** Last updated on 09/07/2013 - Twitter API Fix **/
function start() {
Logger.log("start!" + '\n')
// REPLACE THESE DUMMY VALUES
// https://script.google.com/macros/d/18DGYaa-jbaAK9rEv0HZ2cMcWjFGgkvVcvr6TfksMNbbu2Brk3gZeZ46R/edit
var TWITTER_CONSUMER_KEY = "___REDACTED___";
var TWITTER_CONSUMER_SECRET = "___REDACTED___";
var TWITTER_HANDLE = "___REDACTED___";
var SEARCH_QUERY = "___REDACTED___" + TWITTER_HANDLE;
// Store variables
ScriptProperties.setProperty("TWITTER_CONSUMER_KEY", TWITTER_CONSUMER_KEY);
ScriptProperties.setProperty("TWITTER_CONSUMER_SECRET", TWITTER_CONSUMER_SECRET);
ScriptProperties.setProperty("TWITTER_HANDLE", TWITTER_HANDLE);
ScriptProperties.setProperty("SEARCH_QUERY", SEARCH_QUERY);
ScriptProperties.setProperty("SINCE_TWITTER_ID", "404251049889759234");
// Delete exiting triggers, if any
var triggers = ScriptApp.getScriptTriggers();
for(var i=0; i < triggers.length; i++) {
ScriptApp.deleteTrigger(triggers[i]);
}
// Setup trigger to read Tweets every 2 hours
ScriptApp.newTrigger("fetchTweets")
.timeBased()
.everyMinutes(30)
//.everyHours(2)
.create();
}
function oAuth() {
//Authentication
var oauthConfig = UrlFetchApp.addOAuthService("twitter");
oauthConfig.setAccessTokenUrl("https://api.twitter.com/oauth/access_token");
oauthConfig.setRequestTokenUrl("https://api.twitter.com/oauth/request_token");
oauthConfig.setAuthorizationUrl("https://api.twitter.com/oauth/authorize");
oauthConfig.setConsumerKey(ScriptProperties.getProperty("TWITTER_CONSUMER_KEY"));
oauthConfig.setConsumerSecret(ScriptProperties.getProperty("TWITTER_CONSUMER_SECRET"));
}
function fetchTweets() {
oAuth();
// I put this line in to monitor whether the property is getting "stored" so as to avoid
// reading in duplicate tweets.
Logger.log("Getting tweets since " + ScriptProperties.getProperty("SINCE_TWITTER_ID"))
var twitter_handle = ScriptProperties.getProperty("TWITTER_HANDLE");
var search_query = ScriptProperties.getProperty("SEARCH_QUERY")
Logger.log("searching tweets to " + search_query + '\n');
// form the base URL
// restrict to a certain radius ---:
//var search = "https://api.twitter.com/1.1/search/tweets.json?count=5&geocode=42.827934,-83.564306,75mi&include_entities=false&result_type=recent&q=";
// unrestricted radius:
var search = "https://api.twitter.com/1.1/search/tweets.json?count=5&include_entities=false&result_type=recent&q=";
search = search + encodeString(search_query) + "&since_id=" + ScriptProperties.getProperty("SINCE_TWITTER_ID");
var options =
{
"method": "get",
"oAuthServiceName":"twitter",
"oAuthUseToken":"always"
};
try {
var result = UrlFetchApp.fetch(search, options);
var lastID = ScriptProperties.getProperty("SINCE_TWITTER_ID");
if (result.getResponseCode() === 200) {
var data = Utilities.jsonParse(result.getContentText());
if (data) {
var tweets = data.statuses;
//Logger.log(data.statuses);
for (var i=tweets.length-1; i>=0; i--) {
// Make sure this is a NEW tweet
if (tweets[i].id > ScriptProperties.getProperty("SINCE_TWITTER_ID")) {
lastID = (tweets[i].id_str);
var answer = tweets[i].text.replace(new RegExp("\#" + twitter_handle, "ig"), "").replace(twitter_handle, "");
// I find this TRY block may be necessary since a failure to send one of the tweets
// may abort the rest of the loop.
try {
Logger.log("found >> " + tweets[i].text)
Logger.log("converted >> " + answer + '\n');
sendTweet(tweets[i].user.screen_name, tweets[i].id_str, answer.substring(0,140));
// Update the script property to avoid duplicates.
ScriptProperties.setProperty("SINCE_TWITTER_ID", lastID);
Logger.log("sent to #" + tweets[i].user.screen_name + '\n');
} catch (e) {
Logger.log(e.toString() + '\n');
}
}
}
}
}
} catch (e) {
Logger.log(e.toString() + '\n');
}
Logger.log("Last used tweet.id: " + lastID + + "\n")
}
function sendTweet(user, reply_id, tweet) {
var options =
{
"method": "POST",
"oAuthServiceName":"twitter",
"oAuthUseToken":"always"
};
var status = "https://api.twitter.com/1.1/statuses/update.json";
status = status + "?status=" + encodeString("RT #" + user + " " + tweet + " - Thanks\!");
status = status + "&in_reply_to_status_id=" + reply_id;
try {
var result = UrlFetchApp.fetch(status, options);
Logger.log("JSON result = " + result.getContentText() + '\n');
}
catch (e) {
Logger.log(e.toString() + '\n');
}
}
// Thank you +Martin Hawksey - you are awesome
function encodeString (q) {
// Update: 09/06/2013
// Google Apps Script is having issues storing oAuth tokens with the Twitter API 1.1 due to some encoding issues.
// Henc this workaround to remove all the problematic characters from the status message.
var str = q.replace(/\(/g,'{').replace(/\)/g,'}').replace(/\[/g,'{').replace(/\]/g,'}').replace(/\!/g, '|').replace(/\*/g, 'x').replace(/\'/g, '');
return encodeURIComponent(str);
// var str = encodeURIComponent(q);
// str = str.replace(/!/g,'%21');
// str = str.replace(/\*/g,'%2A');
// str = str.replace(/\(/g,'%28');
// str = str.replace(/\)/g,'%29');
// str = str.replace(/'/g,'%27');
// return str;
}
When you use ScriptProperties.setProperty("KEY", "VALUE");, internally Script Properties will overwrite a duplicate key (i.e., if an old Property has the same key, your new one will replace it). So in your case, since you are using the same identifier for the key (SINCE_TWITTER_ID), it will replace any previous Script Property that is that key.
Furthermore, you can view Script Properties via File -> Project properties -> Project properties (tab). Imo Google didn't name that very well. User properties as specific to Google users. Script properties as specific to the Script Project you are working under.
Also, it probably isn't a good idea to include \n in your value when you set the property. That will lead to all sorts of bugs down the road, because you'll have to compare with something like the following:
var valToCompare = "My value\n";
instead of:
var valToCompare = "My value";
because the value in SINCE_TWITTER_ID will actually be "some value\n" after you call your fetchTweet() function.
Of course, one seems more logical I think, unless you really need the line breaks (in which case you should be using them somewhere else, for this application).
Its ok like that thou I dont know why you are adding \n at fhe end. Might confuse other code. You can see script properties in the script's file menu+ properties