I created a GAS script file on my Drive using the example from Packt's book 'Learning Google Apps Script. My question is how do I make this into an Add-On. Not quite sure on the manifest file I need to create (even after reading https://developers.google.com/gmail/add-ons/how-tos/building).
function saveEmailAttachmentsToDrive(){
// Create 'Gmail Attachments' folder if not exists.
createFolder_('Gmail attachments');
// Get inbox threads starting from the latest one to 100.
var threads = GmailApp.getInboxThreads(0, 100);
var messages = GmailApp.getMessagesForThreads(threads);
var folderID = PropertiesService.getUserProperties()
.getProperty("FOLDER");
var file, folder = DriveApp.getFolderById(folderID);
for (var i = 0 ; i < messages.length; i++) {
for (var j = 0; j < messages[i].length; j++) {
if(!messages[i][j].isUnread()){
var msgId = messages[i][j].getId();
// Assign '' if MSG_ID is undefined.
var oldMsgId = PropertiesService.getUserProperties()
.getProperty('MSG_ID') || '';
if(msgId > oldMsgId){
var attachments = messages[i][j].getAttachments();
for (var k = 0; k < attachments.length; k++) {
PropertiesService.getUserProperties()
.setProperty('MSG_ID', messages[i][j].getId());
try {
file = folder.createFile(attachments[k]);
Utilities.sleep(1000);// Wait before next iteration.
} catch (e) {
Logger.log(e);
}
}
}
else return;
}
}
}
};
function createFolder_(name) {
var folder, folderID, found = false;
/*
* Returns collection of all user folders as an iterator.
* That means it do not return all folder names at once,
* but you should get them one by one.
*
*/
var folders = DriveApp.getFolders();
while (folders.hasNext()) {
folder = folders.next();
if (folder.getName() == name) {
folderID = folder.getId();
found = true;
break;
}
};
if (!found) {
folder = DriveApp.createFolder(name);
folderID = folder.getId();
};
PropertiesService.getUserProperties()
.setProperty("FOLDER", folderID);
return folderID;
}
My question is how do I make this into an Add-On. Not quite sure on the manifest file I need to create (even after reading https://developers.google.com/gmail/add-ons/how-tos/building).
The JSON for the manifest file for the gmail part is:
"gmail": {
"name": "My Gmail Add-on",
"logoUrl": "https://www.example.com/hosted/images/2x/my-icon.png",
"primaryColor": "#4285F4",
"secondaryColor": "#00BCD4",
"authorizationCheckFunction": "get3PAuthorizationUrls",
"contextualTriggers": [{
"unconditional": {},
"onTriggerFunction": "buildAddOn"
}]
That must be in addition to what is already there. A manifest file at it's very basic, looks like this:
{
"timeZone": "America/New_York",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER"
}
You are going to make it look like the following:
{
"timeZone": "America/New_York",
"dependencies": {
},
"gmail": {
"name": "My Gmail Add-on",
"logoUrl": "https://www.example.com/hosted/images/2x/my-icon.png",
"primaryColor": "#4285F4",
"secondaryColor": "#00BCD4",
"authorizationCheckFunction": "get3PAuthorizationUrls",
"contextualTriggers": [{
"unconditional": {},
"onTriggerFunction": "buildAddOn"
}]
},
"exceptionLogging": "STACKDRIVER"
}
Use your timeZone, and your settings. Note that there are no dependencies listed, but leave that part in.
manifest.json is generated automatically by the Apps Script engine. It is hidden by default, but you can view it by toggling View > manifest file to see the structure.
When you're ready to test the AddOn, you can go to Publish > Deploy as web add on. If you're uploading from the cloud editor, you do not need to add a separate manifest file in the web store listing.
The development documentation covers deployment methods in detail.
Related
I'm new to Apps Script and I'm trying to make a script that finds all the empty folders and change their color. I managed to do it, when I try it in my Drive it works perfectly, but when I try run it in the Shared Drive it gives me this error:
Exception: Request failed for https://www.googleapis.com returned code 404. Truncated server response: {
"error": {
"errors": [
{
"domain": "global",
"reason": "notFound",
"message": "File not found: 166gK63I72FZvqY0XjSE63z4SyQuSgGp... (use muteHttpExceptions option to examine full response)
I searched around for some solutions, the only one being to create a project in Google Cloud Platform. I did so, and linked my script to the project in GCP but it gives the same error.
Basically, I want my script to have access to my Shared Drive. The Shared Drive it's pretty big, it has a lot of folders and files, I don't know if maybe the error is due to too many api calls, or if it's a problem with the permission.
I hope someone can help! Thanks a lot! I leave you here my script:
function findEmptyFolders() {
var parentFolder = DriveApp.getFolderById('Shared Drive ID');
var folders = parentFolder.getFolders();
while (folders.hasNext()) {
var childfolder = folders.next();
recurseFolder(parentFolder, childfolder);
}
}
function recurseFolder(parentFolder, folder) {
var filecount = 0;
var foldercount = 0;
var files = folder.getFiles();
var childfolders = folder.getFolders();
while (childfolders.hasNext()) {
var childfolder = childfolders.next();
recurseFolder(folder, childfolder);
foldercount++;
}
while (files.hasNext()) {
var file = files.next();
filecount++;
}
if (filecount == 0 && foldercount == 0) {
var id = folder.getId();
Logger.log(folderColor.setColorByName(id,'Yellow cab'));
}
}
var folderColor = {};
folderColor.setColorByName = function(id,name){
if(!colorPalette[name]){
throw "Name is not valid, please check name in colorPalette.";
}
this.setColor(id,colorPalette[name]);
return true;
}
folderColor.init = function(){
return this;
}
folderColor.setColor = function(id,hexa){
var url = 'https://www.googleapis.com/drive/v2/files/'+id+'?fields=folderColorRgb';
var param = {
method : "patch",
contentType: 'application/json',
headers : {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
payload: JSON.stringify({folderColorRgb:hexa})
};
var html = UrlFetchApp.fetch(url,param).getContentText();
return html;
}
var colorPalette = {
"Chocolate ice cream":"#ac725e",
"Old brick red":"#d06b64",
"Cardinal":"#f83a22",
"Wild straberries":"#fa573c",
"Mars orange":"#ff7537",
"Yellow cab":"#ffad46",
"Spearmint":"#42d692",
"Vern fern":"#16a765",
"Asparagus":"#7bd148",
"Slime green":"#b3dc6c",
"Desert sand":"#fbe983",
"Macaroni":"#fad165",
"Sea foam":"#92e1c0",
"Pool":"#9fe1e7",
"Denim":"#9fc6e7",
"Rainy sky":"#4986e7",
"Blue velvet":"#9a9cff",
"Purple dino":"#b99aff",
"Mouse":"#8f8f8f",
"Mountain grey":"#cabdbf",
"Earthworm":"#cca6ac",
"Bubble gum":"#f691b2",
"Purple rain":"#cd74e6",
"Toy eggplant":"#a47ae2"
};
In your script, how about the following modification?
From:
var url = 'https://www.googleapis.com/drive/v2/files/'+id+'?fields=folderColorRgb';
To:
var url = 'https://www.googleapis.com/drive/v2/files/'+id+'?fields=folderColorRgb&supportsAllDrives=true';
Reference:
Files: patch
NEW INFORMATION
Nov 17, 2020 - I can run onOpen from the script without getting the permission error. I can run the function that displays data from the script or the menu (after I have run onOpen from the script). I don't think there were any changes to the code but here it is again. The manifest is definitely unchanged.
//#NotOnlyCurrentDoc
function globalVariables(){
var variables = {
// this is the Connect Four Game
sheetId: '1fmZCittj4ksstmhh8_t0O0csj8IDdwi9ohDDL5ZE7VA',
sheetUrl: 'https://docs.google.com/spreadsheets/d/1fmZCittj4ksstmhh8_t0O0csj8IDdwi9ohDDL5ZE7VA/edit?usp=sharing'
};
return variables; //return a dictionary of keys and values
}
function onOpen() {
Logger.log("In onOpen" );
var variables = globalVariables(); //load the Global variables
Logger.log("In onOpen sheetUrl from globalVariables: " + variables.sheetUrl );
try {
// var ss = SpreadsheetApp.openByUrl(variables.sheetUrl);
var ss = SpreadsheetApp.openById(variables.sheetId)
if (!ss) {
SlidesApp.getUi().alert("Spreadsheet not found!");
return;
}
} catch(e) {
SlidesApp.getUi().alert(e);
return;
}
SlidesApp.getUi()
.createMenu( 'Ask ?')
.addItem('BE1','BE1')
.addItem('BE2','BE2')
.addItem('BE3','BE3')
.addItem('BE4','BE4')
.addToUi();
prepareQuestions(ss);
Logger.log("After prepareQuestions");
setupSheets(ss); // only want to do this once
Logger.log("After setupSheets");
}
function setupSheets(ss) {
SlidesApp.getUi().alert("setupSheets() triggered");
var nextI = '1';
var cache = CacheService.getUserCache();
cache.put('nextI', nextI);
console.log("1. cache.put('nextI') = " + nextI);
SlidesApp.getUi().alert("End onOpne) = ");
}
function BE1() {
SlidesApp.getUi().alert("BE1() triggered");
var variables = globalVariables(); //load the Global variables
var sheetName = 'BE1';
var sheet = SpreadsheetApp.openById(variables.sheetId).getSheetByName('BE1');
if (!sheet) {
Logger.log("Sheet BE1 not found");
SlidesApp.getUi().alert("Sheet BE1 not found");
return;
}
getNextQ(sheet);
SlidesApp.getUi().alert("end BE1 function");
}
I am the creator of both the spreadsheet and the slides. They are in the same folder on My Drive.
I have added Oauth to the manifest in the Slide:
{
"timeZone": "America/Mexico_City",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.readonly"
]
}
I have tried to open the Spreadsheet by ID and URL. The error is:
Exception: You do not have permission to call SpreadsheetApp.openById. Required permissions: https://www.googleapis.com/auth/spreadsheets
I have tried creating a trigger (I have no idea what this is for) but when creating it the only options were time based which does not seem useful. The presence of the trigger does not change the error.
The code bound to the Slide
//#NotOnlyCurrentDoc
function globalVariables(){
var variables = {
// this is the Connect Four Game
sheetId: '1fmZCittj4ksstmhh8_t0O0csj8IDdwi9ohDDL5ZE7VA',
sheetUrl: 'https://docs.google.com/spreadsheets/d/1fmZCittj4ksstmhh8_t0O0csj8IDdwi9ohDDL5ZE7VA/edit?usp=sharing'
};
return variables; //return a dictionary of keys and values
}
//function installableOpen() { // never starts
function onOpen() {
var variables = globalVariables(); //load the Global variables
try {
// var ss = SpreadsheetApp.openByUrl(variables.sheetUrl);
var ss = SpreadsheetApp.openById(variables.sheetId)
if (!ss) {
SlidesApp.getUi().alert("Spreadsheet not found!");
return;
}
} catch(e) {
SlidesApp.getUi().alert(e);
return;
}
etc...
I added "//#NotOnlyCurrentDoc" as this was recommended in some places.
Share status of spreadsheet
The purpose of accessing the spreadsheet is to get data to display in html with showModalDialog. I have wandered through information on custom menus, dialogs, and sidebars to Google Docs, Sheets, and Forms, custom functions and macros, web apps, add-ons but I don't understand any of these. I was hoping to keep this simple. Custom function says it cannot display html (https://developers.google.com/apps-script/guides/sheets/functions) so that would be useless.
Any ideas appreciated.
As result of comment I changed the manifest as follows but the error is unchanged.
{
"timeZone": "America/Mexico_City",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets"
]
}
= = = = = = = = = = = = = = = = = = = =
I received this sort of email after deleting the triggers
As the error message says:
For the method
SpreadsheetApp.openById()
you are required to provide the scope
https://www.googleapis.com/auth/spreadsheets
instead of
https://www.googleapis.com/auth/spreadsheets.readonly
If you do not want to provide this scope, consider binding your script to the spreadsheet and use the method
SpreadsheetApp.getActive()
which accepts the scope
https://www.googleapis.com/auth/spreadsheets.currentonly
CRAZY FIX - I connected to the slide to which the script was bound before connecting to the spreadsheet! Pure chance discovery but the permission error is gone.
function onOpen() {
var deck = SlidesApp.getActivePresentation();
var slides = deck.getSlides();
var presLen = slides.length;
Logger.log("num slides: " + presLen );
var variables = globalVariables(); //load the Global variables
if (!variables.sheetId) {
Logger.log("SheetId not retrieved from global variables!");
return;
}
try {
var ss = SpreadsheetApp.openById(variables.sheetId);
if (!ss) {
Logger.log("The data spreadsheet does not exist");
SlidesApp.getUi().alert("The data spreadsheet does not exist" + variables.sheetId );
return;
}
} catch (e) {
Logger.log(" in catch for openBYId: " + variables.sheetId );
SlidesApp.getUi().alert(e);
return;
}
I am trying to create an extension to google classroom using google-apps-script but I seem to be running into permission problems- "the caller does not have permission". Can someone tell me why this is happening and what I need to do
I have tried accessing all the libraries provided by google cloud platform hoping that one of them gives the permission that is required all to no avail.
function listCourses() {
var optionalArgs = {
pageSize: 10
};
var response = Classroom.Courses.list(optionalArgs);
var courses = response.courses;
if (courses && courses.length > 0) {
for (i = 0; i < courses.length; i++) {
var course = courses[i];
Logger.log('%s (%s)', course.name, course.id);
var submissions = Classroom.Courses.CourseWork.list(course.id, optionalArgs);
for (i = 0; i < submissions.length; i++) {
var submission = submissions[i];
Logger.log('%s', submission);
}
}
} else {
Logger.log('No courses found.');
}
}
The code is meant to list out the course ids of courses in a particular class
This solution involves doing 2 things -
Updating the manifest file (appsscript.json)
Updating the Code.gs file
Task 1
Updating the manifest file (appsscript.json)
The appropriate Classroom API reference for this task is here.
Looks like even after enabling Advanced Google services..., you only get the following OAuth Scopes added -
https://www.googleapis.com/auth/classroom.courses
https://www.googleapis.com/auth/classroom.coursework.me.readonly
https://www.googleapis.com/auth/classroom.profile.emails
https://www.googleapis.com/auth/classroom.profile.photos
https://www.googleapis.com/auth/classroom.rosters
You can view these by navigating to File > Project properties > Scopes.
However, when you try the API from the documentation link, under the Credentials > Google OAuth 2.0 tab, it shows 4 more, completely different OAuth scopes; those are as follows -
https://www.googleapis.com/auth/classroom.coursework.me
https://www.googleapis.com/auth/classroom.coursework.me.readonly
https://www.googleapis.com/auth/classroom.coursework.students
https://www.googleapis.com/auth/classroom.coursework.students.readonly
You need to add all 8 of these manually in your Apps Script manifest file. To do that, navigate to View & check the Show manifest file. There you need to add this code, perhaps below dependencies -
"oauthScopes": [
"https://www.googleapis.com/auth/classroom.courses",
"https://www.googleapis.com/auth/classroom.coursework.me.readonly",
"https://www.googleapis.com/auth/classroom.profile.emails",
"https://www.googleapis.com/auth/classroom.profile.photos",
"https://www.googleapis.com/auth/classroom.rosters",
"https://www.googleapis.com/auth/classroom.coursework.me",
"https://www.googleapis.com/auth/classroom.coursework.me.readonly",
"https://www.googleapis.com/auth/classroom.coursework.students",
"https://www.googleapis.com/auth/classroom.coursework.students.readonly"
],
Note1: Only adding the newer 4 will not do the trick as the script would assume only these and not the original 5 that were auto-populated when your script ran for the first time.
Note2: The blank line is simply to differentiate between the scopes that get generated automatically vs. the ones you need to add manually (its redundant).
My appsscript.json file looks like this; yours might differ -
{
"timeZone": "Asia/Kolkata",
"dependencies": {
"enabledAdvancedServices": [{
"userSymbol": "Classroom",
"serviceId": "classroom",
"version": "v1"
}]
},
"oauthScopes": [
"https://www.googleapis.com/auth/classroom.courses",
"https://www.googleapis.com/auth/classroom.coursework.me.readonly",
"https://www.googleapis.com/auth/classroom.profile.emails",
"https://www.googleapis.com/auth/classroom.profile.photos",
"https://www.googleapis.com/auth/classroom.rosters",
"https://www.googleapis.com/auth/classroom.coursework.me",
"https://www.googleapis.com/auth/classroom.coursework.me.readonly",
"https://www.googleapis.com/auth/classroom.coursework.students",
"https://www.googleapis.com/auth/classroom.coursework.students.readonly"
],
"exceptionLogging": "STACKDRIVER"
}
Task 2
Updating the Code.gs file
Once you have the right permissions, you're then free to play around with the actual code - the one you've originally shared lacks a few components and I've modified the same to get it working here -
function listCourses() {
var optionalArgs = {
pageSize: 10
};
var response = Classroom.Courses.list(optionalArgs);
var courses = response.courses;
if (courses && courses.length > 0) {
for (var i = 0; i < courses.length; i++) {
var course = courses[i];
Logger.log('%s (%s)', course.name, course.id);
var submissions = Classroom.Courses.CourseWork.list(course.id, optionalArgs);
for (var j = 0; j < submissions.courseWork.length; j++) {
var submission = submissions.courseWork[j];
Logger.log('%s', submission);
}
}
} else {
Logger.log('No courses found.');
}
}
Hope this helps :) but I don't actually know why these scopes are not added automatically, perhaps open up an issue with Google for it.
Edit note: Rectified grammatical errors
I have over 350,000 RingCentral Call logs I want to upload to BigQuery so I can use SQL queries to pull and digest in reports. They are currently stored as 23 .csv files to keep each under the 10MB limit imposed by the BigQuery API. I want to use Google Apps Script to upload the CSV data so all 350K entries are in one table. Here is the code I have so far:
function uploadCSVtoBigQuery() {
try {
var CSVFolder = "Drive ID to Folder A";
var ProcessedFolder = "Drive ID to Folder B";
var projectId = 'ring-central-call-logs';
var datasetId = 'RingCentral';
var tableId = 'Calls';
//CSVFolder = getDriveFolder(CSVFolder);
var CSVFolder = DriveApp.getFolderById(CSVFolder);
//ProcessedFolder = getDriveFolder(ProcessedFolder);
var ProcessedFolder = DriveApp.getFolderById(ProcessedFolder);
if (CSVFolder && ProcessedFolder) {
Logger.log("Folders Appear Valid");
var data, job, file, files = CSVFolder.getFiles();
while (files.hasNext()) {
file = files.next();
if (file.getMimeType() === "text/csv") {
data = file.getBlob().setContentType('application/octet-stream');
job = {
configuration: {
load: {
destinationTable: {
projectId: projectId,
datasetId: datasetId,
tableId: tableId
},
skipLeadingRows: 1
}
}
};
job = BigQuery.Jobs.insert(job, projectId, data);
file.makeCopy(file.getName(), ProcessedFolder);
file.setTrashed(true);
Logger.log('Job status for %s https://bigquery.cloud.google.com/jobs/%s', file.getName(), projectId);
} else{Logger.log(file.getMimeType()+" Is not a valid file type.");}
}
} else{Logger.log('One of your folders is not valid');}
Logger.log('Finished');
} catch(e) {
Logger.log(e.toString());
}
}
// Return the ID of the Google Drive nested folder
function getDriveFolder(name) {
var results, folders = name.split("\\");
var folder = DriveApp.getRootFolder();
for (var i=0; i<folders.length; i++) {
if (folders[i] === "") continue;
results = folder.getFoldersByName(folders[i]);
if (results.hasNext()) {
folder = results.next();
} else {
folder = folder.createFolder(folders[i]);
}
}
return folder;
}
On running the function I get a JSON response as follows:
GoogleJsonResponseException: Using table ring-central-call-logs:RingCentral.Calls is not allowed for this operation because of its type. Try using a different table that is of type TABLE.
To check my brain, I tried running the REST API in the API Explorer, like this:
POST https://www.googleapis.com/bigquery/v2/projects/ring-central-call-logs/datasets/RingCentral/tables/Calls/insertAll?key={YOUR_API_KEY}
{
"skipInvalidRows": true,
"rows": [
{
"json": {
"User": "Nathaniel",
"CallStartTime": "2018-07-09 15:45:10",
"From": "123456789",
"To": "9876543211",
"DurationSeconds": "15",
"CallDirection": "out",
"CallResult": "Failed",
"QueueName": "Test Queue"
}
}
],
"kind": "bigquery#tableDataInsertAllRequest"
}
And got this similar error:
400
- Show headers -
{
"error": {
"code": 400,
"message": "Cannot add rows to a table of type EXTERNAL.",
"errors": [
{
"message": "Cannot add rows to a table of type EXTERNAL.",
"domain": "global",
"reason": "invalid"
}
],
"status": "INVALID_ARGUMENT"
}
}
Screenshot here for Schema looks to match what I am inputting:
I would suppose I have permissions issue by reading it, but I can run a Query function in GAS just fine. Could I get assistance to figure out why this isn't working? Thank you!
This is my first post here and I am new to coding. I have been tasked with creating an automated report which will send a google form submitter a graph to help them monitor their production versus their daily goal. To do this I am using the new developer Google sheets script to refresh a pivot table. I found this code online, and it works great, however, I want to add a line which will filter based to the unique submitter's data. Here is the code I have so far:
function updatePivotTable() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var pivotTableSheetName = "Lunch Chart";
var pivotTableSheetId = ss.getSheetByName(pivotTableSheetName).getSheetId();
var fields = "sheets(properties.sheetId,data.rowData.values.pivotTable)";
var sheets = Sheets.Spreadsheets.get(ss.getId(), {fields: fields}).sheets;
for (var i in sheets) {
if (sheets[i].properties.sheetId == pivotTableSheetId) {
var pivotTableParams = sheets[i].data[0].rowData[0].values[0].pivotTable;
break;
}
}
// Update source range:
pivotTableParams.source.endRowIndex = 40;
// Send back the updated params
var request = {
"updateCells": {
"rows": {
"values": [{
"pivotTable": pivotTableParams
}]
},
"start": {
"sheetId": pivotTableSheetId
},
"fields": "pivotTable"
}
};
Sheets.Spreadsheets.batchUpdate({'requests': [request]}, ss.getId());
}
Is this possible? Where would I add in the filter piece? I found this on the google developer site, but I am very new to coding so I don't really know where to put it or how to make it conditional. https://developers.google.com/sheets/api/reference/rest/v4/FilterCriteria
Thank you!
I am not sure if this is still relevant to you but you can add a filter using following piece of code-
"criteria": {
<col_index>: {"visibleValues": <filter criteria>},
<col_index>: {"visibleValues": <filter criteria>},
I've had the same problem and didn't find an easy explanation or code. Here is what I've done and it works:
function updatePivotTable() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var pivotTableSheetName = "Pivot";
var pivotTableSheetId = ss.getSheetByName(pivotTableSheetName).getSheetId();
var fields = "sheets(properties.sheetId,data.rowData.values.pivotTable)";
var sheets = Sheets.Spreadsheets.get(ss.getId(), {fields: fields}).sheets;
for (var i in sheets) {
if (sheets[i].properties.sheetId == pivotTableSheetId) {
var pivotTableParams = sheets[i].data[0].rowData[0].values[0].pivotTable;
break;
}
}
// Update source range:
pivotTableParams.source.endRowIndex = 111;
pivotTableParams.criteria = { 3: {"visibleValues": ["foo"]}};
// Send back the updated params
var request = {
"updateCells": {
"rows": {
"values": [{
"pivotTable": pivotTableParams
}]
},
"start": {
"sheetId": pivotTableSheetId
},
"fields": "pivotTable",
}
};
Sheets.Spreadsheets.batchUpdate({'requests': [request]}, ss.getId());
}
So basically you need to pass the criteria as a pivotTableParams where the key of the object is the index of the column that you want to query and the value should be passed as another object with the format {"visibleValues": ["foo"]}.