Using Cache from Server Handler - google-apps-script

I am trying to save some user input from a Google App Script form on a spreadsheet to the private cache.
Here is a test script:
var cache = CacheService.getPrivateCache();
function onLoad() {
var sheet = SpreadsheetApp.getActiveSpreadsheet(),
entries = [{
name : "Show form",
functionName : "showForm"
}];
sheet.addMenu("Test Menu", entries);
}
function showForm() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet(),
app = UiApp.createApplication(),
setButton = app.createButton("Set"),
setHandler = app.createServerClickHandler('setTest');
setButton.addClickHandler(setHandler);
app.add(setButton);
spreadsheet.show(app);
}
function setTest(event) {
cache.put("test", "test", 7200);
Browser.msgBox("Test was set: " + cache.get("test") + ". Use the getTest cell formula to test the cache.");
}
function getTest() {
var result = cache.get("test");
return result;
}
Once I click the menu button, a form appears and sets a cache value in a server handler. Then I try to get the value from the cache using =getTest() in a cell. I would expect the cache to return the value "test" but it seems to return null.
I started this issue here:
http://code.google.com/p/google-apps-script-issues/issues/detail?id=2039
And I also found another similar one:
http://code.google.com/p/google-apps-script-issues/issues/detail?id=1804
Trying to save some user input on a form into the cache and be able to access it from another function at a later time.
Any suggestions?

Custom functions have limited scope and then can only access a few select services. You read more about their limitations here.
With Cache its a bit tricky - caching is per "script scope". So when a Cache is accessed first from the Server handler script scope its running as a UiApp which has more privileges and that is different from the script scope that a custom function runs as. Therefore, the cache is not shared between those two scopes.
You can access cached items that you set in custom functions from other custom functions, but this cache can't cross these boundaries.
You could theoretically store this in ScripProperties but that can very clumsy and it would be misusing those capabilities and this is shared amongst all users.
ScriptProperties.setProperty("test", "test")
and
var result = ScriptProperties.getProperty("test");
If you can explain your usecase a bit more in depth, perhaps we can provide some alternative solutions.

Related

Hiding google sheet tab from users [duplicate]

This question already has an answer here:
Google Spreadsheet - Show sheets depending on type of user
(1 answer)
Closed 2 years ago.
I have a google sheets document with two tabs one called called internal and the other called external. How can i hide the internal tab from other users? the lock function already avialble is not good enough I only want people from my company to be able to see both tabs, clients should only be able to see the external tab.
function validUsers() {
String[] adminUsers = {”email1#gmail.com”,”email2#gmail.com”,”email3#gmail.com”};
if (adminUsers.indexOf(Session.getEffectiveUser().getEmail()) >= 0) {
SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Internal').showSheet()
else
SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Internal').hideSheet()
}
}
Issue:
You want to hide or show a sheet in your spreadsheet depending on which user is accessing the spreadsheet.
Solution:
You could do the following:
Install an onOpen trigger which executes a function (let's call it fireOnOpen) every time a user opens the spreadsheet.
The function fireOnOpen should check which user is accessing the spreadsheet, and hide or show a certain sheet (called Internal) depending on this.
In order to check the current user accessing the spreadsheet, you can use getActiveUser() (instead of getEffectiveUser(), which will return the user who installed the trigger).
Workflow:
The trigger can be installed either manually or programmatically. To do it programmatically, copy this function to your script editor and execute it once:
function createOnOpenTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger("fireOnOpen")
.forSpreadsheet(ss)
.onOpen()
.create();
}
This will result in fireOnOpen being executed every time a user accessed the spreadsheet. The fireOnOpen function could be something like this:
function fireOnOpen() {
const adminUsers = ["email1#gmail.com","email2#gmail.com","email3#gmail.com"];
const currentUser = Session.getActiveUser().getEmail();
const internalSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Internal");
if (adminUsers.includes(currentUser)) internalSheet.showSheet();
else internalSheet.hideSheet();
}
Important notes:
You cannot hide sheets for some users but not for others. A hidden sheet is hidden for all users, and a visible sheet is visible for all users. Therefore, this will only work if internal and external users don't access the spreadsheet at the same time. If they do, external users might be able to access the Internal sheet.
getActiveUser() is not always populated, as you can see on this answer, so please make sure that all admin users are from the same G Suite domain. Otherwise, this won't work.
If the privacy of the Internal sheet is critical and there is a possibility of internal and external users accessing the spreadsheet at the time, I would not recommend this solution.
Edit:
As mentioned in comments, a possible workaround for the occasions when admin and non-admin users access the file at the time could be the following:
When an admin user accesses the file, store the time in which that happened.
Create a time-driven trigger to execute a function periodically (every 5 minutes, let's say), which will check if an admin accessed the file a short time ago (let's say 30 minutes). If the admin has done that, remove the Permissions for the different non-admin domains. If that's not the case, add these Permissions back.
Enabling the Drive Advanced Service would be required in this case.
Updated code sample:
function fireOnOpen() {
const adminUsers = ["email1#gmail.com","email2#gmail.com","email3#gmail.com"];
const currentUser = Session.getActiveUser().getEmail();
const internalSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Internal");
if (adminUsers.includes(currentUser)) {
internalSheet.showSheet();
const documentProperties = PropertiesService.getDocumentProperties();
documentProperties.setProperty("lastAdminAccess", new Date().getTime()); // Store time of admin access
} else internalSheet.hideSheet();
}
function createOnOpenTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger("fireOnOpen")
.forSpreadsheet(ss)
.onOpen()
.create();
}
function updatePermissions() {
const fileId = SpreadsheetApp.getActive().getId();
const lastAdminAccess = PropertiesService.getDocumentProperties().getProperty("lastAdminAccess"); // Last time of admin access in ms
const now = new Date().getTime(); // Current time in milliseconds
const thirtyMinutes = 1000 * 60 * 30; // 30 minutes in milliseconds
if (now - lastAdminAccess < thirtyMinutes) {
const currentPermissions = Drive.Permissions.list(fileId)["items"];
const publicPermissionIds = currentPermissions.filter(permission => permission["type"] === "anyone")
.map(permission => permission["id"]);
publicPermissionIds.forEach(permissionId => Drive.Permissions.remove(fileId, permissionId));
} else {
const resource = {
type: "anyone",
role: "reader"
}
Drive.Permissions.insert(resource, fileId);
}
}
function createTimeTrigger() {
ScriptApp.newTrigger("updatePermissions")
.timeBased()
.everyMinutes(5)
.create();
}
As soon as you share a sheet you should assume that anyone can see the data in it. Even if someone shouldn't be able to see the internal tab, they can always e.g. make a copy of the sheet and thus get to the data.
You could try creating a separate sheet and using =IMPORTRANGE() to refer to the original one. But know that once you allow the connection between the two sheets, anyone with access to the second one might be able to access anything in the first one. Maybe get around that using three sheets:
Internal + External - your current sheet
A sheet-in-the-middle that only you can access. It has a single tab Internal that uses =IMPORTRANGE() to access data from 1)
The External sheet for clients. Linked to 2) through =IMPORTRANGE()
This way 3) only has access to the data in 2) which in turn only includes a link to 1).
I do not promise that this will make the data safe from those who shouldn't see it. But it will at least be safer.

Return Collection of Google Drive Files Shared With Specific User

I'm trying to get a collection of files where user (let's use billyTheUser#gmail.com) is an editor.
I know this can be accomplished almost instantly on the front-end of google drive by doing a search for to:billyTheUser#gmail.com in the drive search bar.
I presume this is something that can be done in Google App Scripts, but maybe I'm wrong. I figured DriveApp.searchFiles would work, but I'm having trouble structuring the proper string syntax. I've looked at the Google SDK Documentation and am guessing I am doing something wrong with the usage of the in matched to the user string search? Below is the approaches I've taken, however if there's a different method to accomplishing the collection of files by user, I'd be happy to change my approach.
var files = DriveApp.searchFiles(
//I would expect this to work, but this doesn't return values
'writers in "billyTheUser#gmail.com"');
//Tried these just experimenting. None return values
'writers in "to:billyTheUser#gmail.com"');
'writers in "to:billyTheUser#gmail.com"');
'to:billyTheUser#gmail.com');
// this is just a test to confirm that some string searches successfully work
'modifiedDate > "2013-02-28" and title contains "untitled"');
Try flipping the operands within the in clause to read as:
var files = DriveApp.searchFiles('"billyTheUser#gmail.com" in writers');
Thanks #theAddonDepot! To illustrate specifically how the accepted answer is useful, I used it to assist in building a spreadsheet to help control files shared with various users. The source code for the full procedure is at the bottom of this post. It can be used directly within this this google sheet if you copy it.
The final result works rather nicely for listing out files by rows and properties in columns (i.e. last modified, security, descriptions... etc.).
The ultimate purpose is to be able to update large number of files without impacting other users. (use case scenario for sudden need to immediately revoke security... layoffs, acquisition, divorce, etc).
//code for looking up files by security
//Posted on stackoverlow here: https://stackoverflow.com/questions/62940196/return-collection-of-google-drive-files-shared-with-specific-user
//sample google File here: https://docs.google.com/spreadsheets/d/1jSl_ZxRVAIh9ULQLy-2e1FdnQpT6207JjFoDq60kj6Q/edit?usp=sharing
const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("FileList");
const clearRange = true;
//const clearRange = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("ClearRange").getValue();
//if you have the named range setup.
function runReport() {
//var theEmail= SpreadsheetApp.getActiveSpreadsheet().getRangeByName("emailFromExcel").getValue();
//or
var theEmail = 'billyTheUser#gmail.com';
findFilesByUser(theEmail);
}
function findFilesByUser(theUserEmail) {
if(clearRange){
ss.getDataRange().offset(1,0).deleteCells(SpreadsheetApp.Dimension.ROWS)
}
var someFiles = DriveApp.searchFiles('"' + theUserEmail + '" in writers');
var aListOfFiles = []
while(someFiles.hasNext()){
var aFile = someFiles.next();
aListOfFiles.push([aFile.getId()
,aFile.getName()
,aFile.getDescription()
,aFile.getSharingAccess()
,aFile.getSharingPermission()
,listEmails(aFile.getEditors())
,listEmails(aFile.getViewers())
,aFile.getMimeType().replace('application/','').replace('vnd.google-apps.','')
,aFile.getDateCreated()
,aFile.getLastUpdated()
,aFile.getSize()
,aFile.getUrl()
,aFile.getDownloadUrl()
])
}
if(aListOfFiles.length==0){
aListOfFiles.push("no files for " + theUserEmail);
}
ss.getRange(ss.getDataRange().getLastRow()+1,1, aListOfFiles.length, aListOfFiles[0].length).setValues(aListOfFiles);
}
function listEmails(thePeople){
var aList = thePeople;
for (var i = 0; i < aList.length;i++){
aList[i] = aList[i].getEmail();
}
return aList.toString();
}

Using Google Apps Script, how do you remove built in Gmail category labels from an email?

I'd like to completely undo any of Gmails built in category labels. This was my attempt.
function removeBuiltInLabels() {
var updatesLabel = GmailApp.getUserLabelByName("updates");
var socialLabel = GmailApp.getUserLabelByName("social");
var forumsLabel = GmailApp.getUserLabelByName("forums");
var promotionsLabel = GmailApp.getUserLabelByName("promotions");
var inboxThreads = GmailApp.search('in:inbox');
for (var i = 0; i < inboxThreads.length; i++) {
updatesLabel.removeFromThreads(inboxThreads[i]);
socialLabel.removeFromThreads(inboxThreads[i]);
forumsLabel.removeFromThreads(inboxThreads[i]);
promotionsLabel.removeFromThreads(inboxThreads[i]);
}
}
However, this throws....
TypeError: Cannot call method "removeFromThreads" of null.
It seems you can't access the built in labels in this way even though you can successfully search for label:updates in the Gmail search box and get the correct results.
The question...
How do you access the built in Gmail Category labels in Google Apps Script and remove them from an email/thread/threads?
Thanks.
'INBOX' and other system labels like 'CATEGORY_SOCIAL' can be removed using Advanced Gmail Service. In the Script Editor, go to Resources -> Advanced Google services and enable the Gmail service.
More details about naming conventions for system labels in Gmail can be found here Gmail API - Managing Labels
Retrieve the threads labeled with 'CATEGORY_SOCIAL' by calling the list() method of the threads collection:
var threads = Gmail.Users.Threads.list("me", {labels: ["CATEGORY_SOCIAL"]});
var threads = threads.threads;
var nextPageToken = threads.nextPageToken;
Note that you are going to need to store the 'nextPageToken' to iterate over the entire collection of threads. See this answer.
When you get all thread ids, you can call the 'modify()' method of the Threads collection on them:
threads.forEach(function(thread){
var resource = {
"addLabelIds": [],
"removeLabelIds":["CATEGORY_SOCIAL"]
};
Gmail.Users.Threads.modify(resource, "me", threadId);
});
If you have lots of threads in your inbox, you may still need to call the 'modify()' method several times and save state between calls.
Anton's answer is great. I marked it as accepted because it lead directly to the version I'm using.
This function lets you define any valid gmail search to isolate messages and enables batch removal labels.
function removeLabelsFromMessages(query, labelsToRemove) {
var foundThreads = Gmail.Users.Threads.list('me', {'q': query}).threads
if (foundThreads) {
foundThreads.forEach(function (thread) {
Gmail.Users.Threads.modify({removeLabelIds: labelsToRemove}, 'me', thread.id);
});
}
}
I call it via the one minute script trigger like this.
function ProcessInbox() {
removeLabelsFromMessages(
'label:updates OR label:social OR label:forums OR label:promotions',
['CATEGORY_UPDATES', 'CATEGORY_SOCIAL', 'CATEGORY_FORUMS', 'CATEGORY_PROMOTIONS']
)
<...other_stuff_to_process...>
}

Newly created Google Task omits the supplied "TaskLink" property

I am trying to make a small Google Script that would automatically add Google Tasks to the "My List" TaskList after searching my GMail emails.
Everything goes fine except for adding a link to the email from which the Task is generated from. Trying to follow the API documentation doesn't really help.
This is the code for the actual task generator function:
function addTask(taskListId, myTitle, myEmailLink) {
var task = Tasks.newTask(); // effectively same as "= {}".
task.title = myTitle
task.notes = 'blank';
task.links = [{}]
task.links[0].description = 'Link to corresponding email';
task.links[0].type = 'email';
task.links[0].link = 'myEmailLink';
task = Tasks.Tasks.insert(task, taskListId);
}
Any ideas why the task I receive back has no links?
As others have noted, according to the Google Tasks API Documentation the links collection is unfortunately read-only.
As a potential work around, it appears you can add links to the notes section of a task, and the links are then directly clickable from the tasks pane in GMail.
Picture: Task with clickable link
Your function can be modified to put the link in the notes section as follows:
function addTask(taskListId, myTitle, myEmailLink) {
var task = Tasks.newTask(); // effectively same as "= {}".
task.title = myTitle
task.notes = 'link: ' + myEmailLink;
task = Tasks.Tasks.insert(task, taskListId);
}
Combining this with the getPermalink() function on the GmailApp threads object allows for grabbing a deep link to the email you are looking for.
Picture: Task with permalink to email
I'm working on a set of scripts that do some of the things you're talking about in addition to a few other things: https://github.com/tedsteinmann/gmailAutoUpdate
In my solution I have a function that grabs the GMail threads with a particular label (in my case #Task) and then creates a task setting the subject to thread.getFirstMessageSubject() and the notes to thread.getPermalink()
The entire function looks like this:
function processPending_() {
var label_pending = GmailApp.getUserLabelByName(LABEL_PENDING);
var label_done = GmailApp.getUserLabelByName(LABEL_DONE);
// The threads currently assigned to the 'pending' label
var threads = label_pending.getThreads();
// Process each one in turn, assuming there's only a single
// message in each thread
for (var t in threads) {
var thread = threads[t];
// Grab the task data
var taskTitle = thread.getFirstMessageSubject();
var taskNote = 'Email: ' + thread.getPermalink();
// Insert the task
addTask_(taskTitle, taskNote, getTasklistId_(TASKLIST));
// Set to 'done' by exchanging labels
thread.removeLabel(label_pending);
thread.addLabel(label_done);
}
// Increment the processed tasks count
Logger.log('Processed %s tasks', threads.length);
}
Per the Google Tasks API Documentation:
links[] list
Collection of links. This collection is read-only.
You cannot set these links by modifying a Task resource, i.e your code
task.links = [{}]
task.links[0].description = 'Link to corresponding email';
task.links[0].type = 'email';
task.links[0].link = 'myEmailLink';
is simply ignored by the server.
TaskLinks are, to my knowledge, unusable and non-configurable outside of the Googleplex. They may as well not exist to API users.
The only way I've been able to generate a Task that has one is by using the Gmail UI and selecting "Add to Tasks". The resulting task then includes this snippet in the last line of the Task item:

ScriptDb usage from WebApp

When using ScriptDb from within a WebApp that can be accessed by anyone and runs as the accessing user together with Triggers from within that WebApp it seems as if the user can not be correctly determined.
Google Apps Script as WebApp for Anyone running as the accessing user
ScriptApp.newTrigger("myfunc").timeBased().everyMinutes(1).create();
Where function myfunc is:
var q = {
user: Session.getActiveUser().getEmail()
};
result = db.query(q).sortBy('when', db.ASCENDING);
result seems to be empty when myfunc is being accessed from the trigger.
Shouldn't the active user within the trigger be the one that installed it? E.g. the user accessing the WebApp when first giving the authorization?
The Session.getEffectiveUser method returns the correct user email, as expected. Since when the script is running from a trigger, there's no one "active" using it. The "effective" user though is always set, as it is the account which the script is running "under". Here is a sample web application which emails an expected email address in the subject and body.
function doGet() {
var trigger = ScriptApp.newTrigger("testTrigger").timeBased().everyMinutes(1).create();
var app = UiApp.createApplication();
app.add(app.createLabel("test"));
return app;
}
function testTrigger(e) {
MailApp.sendEmail("xxx#sample.com", "Subject: " + Session.getEffectiveUser().getEmail(), "Body: " + Session.getEffectiveUser().getEmail());
}