Missing publish to Chrome Web Store option for web application - google-apps-script

I'm trying to make an inbox-listener element to automatically pull message content with a specific subject line into a Google Spreadsheet for reporting. I'm hoping this GitHub Project will do the trick, and I've made the Google Apps Script project that should handle it. Part of the process is to verify the project ownership by publishing it as draft on the Chrome web store. According to Google's documentation, that should be under the publish function, but I don't see it there, or anywhere in the script editor. Could anyone tell me what I'm doing wrong, or if this feature is disabled?
Here is what the menu looks like in my IDE.
Adapted from a OP's comment to the accepted answer
The script is definitely not contained in a Google Doc or Spreadsheet, but it is in a workspace tied to a Google Site. Interestingly, I've tried following the link in the KENdi's answer to edit Apps Scripts under my work account, and I'm told I don't have >permission to generate app scripts on that level. I'll send an email to my Admin to see if I can elevate privileges so I can publish through this channel.
GAS Code
main.gs
<meta name="google-site-verification" content="eA0WbBgImGB_wcsnSADjvwnCBaNyrSifyyxuNhHSXf8" />
var PROJECTID = 'api-project-...';
var WEBHOOK_URL = 'My custom project URL'
function doPost(e){
var postBody = JSON.parse(e.postData.getDataAsString());
var messageData = Utilities.newBlob(Utilities.base64Decode(postBody.message.data)).getDataAsString();
var ss = SpreadsheetApp.openById('...').getSheetByName("Log");
ss.appendRow([new Date(), messageData, JSON.stringify(postBody,undefined,2)])
return 200;
}
function setupPubSub(){
var newTopic = CreateTopic("mailTrigger");
newTopic.setIamPolicy(addGmailPolicy());
Logger.log(newTopic.getName());
var newSub = CreateSubscription("mailTrigger",newTopic.getName(),WEBHOOK_URL);
}
function disEnrollEmail(email){
var email = email || "me";
var res = UrlFetchApp.fetch("https://www.googleapis.com/gmail/v1/users/"+email+"/stop",{method:"POST",headers:{authorization:"Bearer "+ScriptApp.getOAuthToken()}});
Logger.log(res.getContentText());
}
function enrollEmail(email){
var email = email || "me";
PubSubApp.setTokenService(getTokenService())
var topicName = PubSubApp.PublishingApp(PROJECTID).getTopicName("mailTrigger")
Logger.log(watchEmail(topicName,{labelIds:["INBOX"], email:email}));
}
helper.gs
function addGmailPolicy(Policy){
return PubSubApp.policyBuilder()
[(Policy)?"editPolicy":"newPolicy"](Policy)
.addPublisher("SERVICEACCOUNT", 'gmail-api-push#system.gserviceaccount.com')
.getPolicy();
}
function addDomainSubs(Domain,Policy){
return PubSubApp.policyBuilder()
[(Policy)?"editPolicy":"newPolicy"](Policy)
.addPublisher("DOMAIN", Domain)
.getPolicy();
}
function getSubscriptionPolicy(){
return PubSubApp.policyBuilder()
.newPolicy()
.addSubscriber("DOMAIN","ccsknights.org")
}
function watchEmail(fullTopicName,watchOptions){
var options = {email:"me",token:ScriptApp.getOAuthToken(),labelIds:[]};
for(var option in watchOptions){
if(option in options){
options[option] = watchOptions[option];
}
}
Logger.log(options);
var url = "https://www.googleapis.com/gmail/v1/users/"+options.email+"/watch"
var payload = {
topicName: fullTopicName,
labelIds: options.labelIds
}
var params = {
method:"POST",
contentType: "application/json",
payload: JSON.stringify(payload),
headers:{Authorization: "Bearer "+ options.token
},
muteHttpExceptions:true
}
var results = UrlFetchApp.fetch(url, params);
if(results.getResponseCode() != 200){
throw new Error(results.getContentText())
}else{
return JSON.parse(results.getContentText());
}
}
function CreateTopic(topicName) {
var topic;
PubSubApp.setTokenService(getTokenService());
var pubservice = PubSubApp.PublishingApp(PROJECTID);
try{topic = pubservice.newTopic(topicName)}
catch(e){topic = pubservice.getTopic(topicName);}
return topic;
}
function CreateSubscription(subscriptionName,topicName,webhookUrl){
var sub;
PubSubApp.setTokenService(getTokenService());
var subService = PubSubApp.SubscriptionApp(PROJECTID);
try{sub = subService.newSubscription(subscriptionName,topicName,webhookUrl)}
catch(e){sub = subService.getSubscription(subscriptionName,topicName,webhookUrl)}
return sub;
}
function getTokenService(){
var jsonKey = JSON.parse(PropertiesService.getScriptProperties().getProperty("jsonKey"));
var privateKey = jsonKey.private_key;
var serviceAccountEmail = jsonKey.client_email;
var sa = GSApp.init(privateKey, ['https://www.googleapis.com/auth/pubsub'], serviceAccountEmail);
sa.addUser(serviceAccountEmail)
.requestToken();
return sa.tokenService(serviceAccountEmail);
}
function requestGmailScope_(){GmailApp.getAliases()}

The "Register in Chrome Web Store" option is only available in the standalone scripts.
Meaning, if this script is bound to a Google Sheets, Docs, Forms or scripts appear among your files in Google Drive, then this "Register in Chrome Web Store" is not available.
You can verify it by checking the publish in this "https://www.google.com/script/start/". This one will have an option of "Register in Chrome Web Store" in the publish.
While by checking the script editor of a spreadsheet, created in your Drive, the "Register in Chrome Web Store" will not be found in the Publish section.
Hope this information helps you.

It turns out I couldn't build Apps Scripts under my work account because my browser was defaulting to my personal Gmail account. Resetting the browser and connecting as my work account first fixed the problem. For future reference, you have to make your scripts under Script.Google.com in order for them to be shareable via the web store. You can bypass an authentication problems like this by resetting your browser history or running Chrome in incognito mode if you have to.
Great catch, KENDi!

Related

How to handle authorization for Apps Script from Chrome extension

I try to execute a Google Apps Script from my Chrome extension. My Google Apps Script uses Spreadsheet Service - it reads data from the user's spreadsheet, url of which is provided by user to my extension and then sent by extension to GAS. So, I've deployed Google Apps Script as web app, which is executed by a user accessing the web app and can be accessed by anyone. The problem is that when the script is run for the first time by a new user, the user can't see the authorization request so the script can't access the spreadsheet and send data to the extension. Everything works fine only if a user runs the script manually in the browser - only then the authorization request appears.
My question is how extension can trigger the authorization request to appear automatically?
I have read this question with answers Running Apps Script from Chrome extension requires authorization but there are only instructions on how to avoid authorization, which in my case can't be avoided. I would appreciate any help with this.
Update: I'm trying to use getAuthorizationUrl() as advised in the comments and now I'm stuck again. Here is the code from the Google Apps Script:
function doGet(e) {
var url=e.parameter.url;
var ss = SpreadsheetApp.openByUrl(url);
var authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);
var status=authInfo.getAuthorizationStatus();
if (status=="NOT_REQUIRED") {
var sheet = ss.getSheets()[0];
var data = getData(sheet);
if(!data) {
data = '';
}
return ContentService.createTextOutput(JSON.stringify({'result': data})).setMimeType(ContentService.MimeType.JSON);
} else {
var authUrl=authInfo.getAuthorizationUrl();
return authUrl;
}
}
In both cases responseType of my XMLHttpRequest will be "", so how can my extension distinguish between 2 possible responses? I need something like this in the content script (don't know what to put inside if):
xhr.onreadystatechange = function(response) {
if (xhr.readyState === 4 && xhr.status == 200) {
try {
if (??response is authUrl) {
//do redirect;
}
else {
//process the data from the spreadsheet
}

Google Apps Script create calendar event from sheet forbidden?

In a document-bound Google Appscript in one of our company spreadsheets, I've created a script that turns spreadsheet lines into Google calendar appointments. The function works fine for me, but not for my coworker, even though we both have permissions to edit the calendar and change sharing permissions, and my coworker proved he can create appointments on the calendar from calendar.google.com.
He gets the following error message when he runs the script:
{"message":"Forbidden","name":"GoogleJsonResponseException","fileName":"SCHEDULER","lineNumber":204,"stack":"\tat SCHEDULER:204 (createAppointments)\n"}
Line 204 corresponds to the command:
Calendar.Events.insert(event, CAL, {sendNotifications: true, supportsAttachments:true});
If he has edit rights to the calendar, why is this forbidden? Is there a problem with the Calendar service in Google Apps Script? What is more, I changed the CAL variable to a calendar I personally created and shared out to him with the same permissions. He can edit that calendar just fine.
Here is the psuedocode for the function
function createAppointments() {
var CAL = 'companyname.com_1v033gttnxe2r3eakd8t9sduqg#group.calendar.google.com';
for(/*each row in spreadsheet*/)
{
if(/*needs appointment*/)
{
var object = {/*...STUFF...*/};
var coworker = 'coworker#companyname.com';
var timeArgs = {start: /*UTC Formatted time*/, end: /*UTC Formatted time*/}
if(/*All the data checks out*/{
var summary = 'Name of appointment'
var notes = 'Stuff to put in the body of the calendar appointment';
var location = '123 Happy Trail, Monterrey, TX 12345'
//BUILD GOOGLE CALENDAR OBJECT
var event = {
"summary": summary,
"description": notes,
"start": {
"dateTime": timeArgs.start,
"timeZone": TZ
},
"end": {
"dateTime": timeArgs.end,
"timeZone": TZ
},
"guestsCanInviteOthers": true,
"reminders": {
"useDefault": true
},
"location": location
//,"attendees": []
};
event.attendees = [{coworker#companyname.com, displayName: 'coworker name'}];
//CREATE CALENDAR IN GOOGLE CALENDAR OF CONST CAL
Calendar.Events.insert(event, CAL, {sendNotifications: true, supportsAttachments:true});
} else{/*Tell user to fix data*/}
}
}
Thank you very much!
Update 12/29/2017:
I've Tried adjusting the app according to Jason Allshorn and Crazy Ivan. Thank you for your help, so far! Interestingly, I have run into the same response using both the Advanced Calendar Service and the CalendarApp.
The error is, as shown below:
<!DOCTYPE html><html><head><link rel="shortcut icon" href="//ssl.gstatic.com/docs/script/images/favicon.ico"><title>Error</title><style type="text/css">body {background-color: #fff; margin: 0; padding: 0;}.errorMessage {font-family: Arial,sans-serif; font-size: 12pt; font-weight: bold; line-height: 150%; padding-top: 25px;}</style></head><body style="margin:20px"><div><img alt="Google Apps Script" src="//ssl.gstatic.com/docs/script/images/logo.png"></div><div style="text-align:center;font-family:monospace;margin:50px auto 0;max-width:600px">Object does not allow properties to be added or changed.</div></body></html>
Or, after parsing that through an html editor:
What does that even mean? I have the advanced service enabled, and the script is enabled to run from anyone. Any ideas?
I have confirmed after testing what the error comes back after trying to run the calendarApp/Advanced Calendar event creation command.
Here is my code that caused me to get this far:
function convertURItoObject(url){
url = url.replace(/\+/g,' ')
url = decodeURIComponent(url)
var parts = url.split("&");
var paramsObj = {};
parts.forEach(function(item){
var keyAndValue = item.split("=");
paramsObj[keyAndValue[0]] = keyAndValue[1]
})
return paramsObj; // here's your object
}
function doPost(e) {
var data = e.postData.contents;
data = convertURItoObject(data);
var CAL = data.cal;
var event = JSON.parse(data.event);
var key = data.key;
var start = new Date(event.start.dateTime);
if(ACCEPTEDPROJECTS.indexOf(key) > -1)
{
try{
var calendar = CalendarApp.getCalendarById(CAL);
calendar.createEvent(event.summary, new Date(event.start.dateTime), new Date(event.end.dateTime), {description: event.description, location: event.location, guests: event.guests, sendInvites: true});}
/*try {Calendar.Events.insert(event, CAL, {sendNotifications: true, supportsAttachments:true});} Same error when I use this command*/
catch(fail){return ContentService.createTextOutput(JSON.stringify(fail));}
e.postData.result = 'pass';
return ContentService.createTextOutput(JSON.stringify(e));
}
else {
return ContentService.createTextOutput('Execution not authorized from this source. See CONFIG of target project for details.');
}
}
Your script is using Advanced Google Services, specifically Calendar. Read the section "Enabling advanced services"; everyone will have to follow those steps to use the script.
Alternatively (in my opinion, this is a better solution), rewrite the script so that it uses the standard CalendarApp service. It also allows you to create an event and then you can add various reminders to that event.
A solution from my side would be to abstract the calendar event creation function away from your Spreadsheet bound script to a separate standalone apps-script that runs under your name with your permissions.
Then from your sheet bound script call to the standalone script with a PUT request containing the information needed to update the Calender. This way anyone using your sheet addon can update the calander without any mess with permissions.
The sheet bound script could look something like this:
function updateCalander(){
var data = {
'event': EVENT,
};
var options = {
'method' : 'post',
'contentType': 'application/json',
'payload' : data
};
var secondScriptID = 'STANDALONE_SCRIPT_ID'
var response = UrlFetchApp.fetch("https://script.google.com/macros/s/" + secondScriptID + "/exec", options);
Logger.log(response) // Expected to see sent data sent back
Then your standalone script would look something like this:
function convertURItoObject(url){
url = url.replace(/\+/g,' ')
url = decodeURIComponent(url)
var parts = url.split("&");
var paramsObj = {};
parts.forEach(function(item){
var keyAndValue = item.split("=");
paramsObj[keyAndValue[0]] = keyAndValue[1]
})
return paramsObj; // here's your object
}
function doPost(e) {
var CAL = 'companyname.com_1v033gttnxe2r3eakd8t9sduqg#group.calendar.google.com';
var data = e.postData.contents;
data = convertURItoObject(data)
var event = data.event;
try {
Calendar.Events.insert(event, CAL, {sendNotifications: true, supportsAttachments:true});
}
catch(e){
Logger.log(e)
}
return ContentService.createTextOutput(JSON.stringify(e));
}
Please note, the standalone script needs to be set to anyone can access, and when you make updates to the code be sure to re-publish the code. If you don't re-publish your calls to the standalone script are not made to the latest code.
This is a delayed response, but thanks to all who recommended using the POST method. It turns out the proper way to do this is to use URLFetchApp and pass the Script's project Key to authorize the calendar access (I believe you only need to make sure the person executing the script has rights to edit the actual calendar).
Here is basically how to do it in a functional way:
//GCALENDAR is th e unique ID of the project int it's URL when the script is open for editing
//PROJECTKEY is the unique ID of the project, found in the Project Properties Menu under FILE.
//CREATE CALENDAR IN GOOGLE CALENDAR OF CONST CAL
var data = {
'event': JSON.stringify(event),
'cal': CAL,
'key': PROJECTKEY
};
var options = {
'method' : 'post',
'contentType': 'application/json',
'payload' : data,
'muteHttpExceptions': true
};
var answer = UrlFetchApp.fetch("https://script.google.com/macros/s/" + GCALENDAR + "/exec", options).getContentText();
Logger.log(answer);

Include sheet names when exporting Google Spreadsheets to PDF with Google Apps Scripts

I am exporting a Google Spreadsheet to PDF format with Google Apps Script, and would like to 'include sheet names' on the PDF pages. Is this possible to do this from the code?
var spreadsheetFile = DocsList.getFileById(spreadsheet_id);
var blob = spreadsheetFile.getAs('application/pdf');
blob.setName(spreadsheetFile.getName());
DocsList.getFolder(file_destination).createFile(blob);
In the Spreadsheet app it is supported in the UI, so I am wondering whether Apps Scripts supports this too?
The .getAs() method does not allow for parameters but you can use the spreadsheet api where you can choose all the parameters available from the "normal" Ui.
See this post answer to see how to use it, and follow the github link
Here is the demo code as there were a few inconsistencies in the code in ref. (just to illustrate with an example exporting sheet1 with grid and title)
note this will ask for 2 distinct authorizations.
function test(){
var key = "0AnqSFd3iikE3dFd1WEVhMFhYczM5VWpuNDZHQ3AwZEE";
var pdf = spreadsheetToPDF(key);
DocsList.createFile(pdf);
}
function spreadsheetToPDF(key) {
var oauthConfig = UrlFetchApp.addOAuthService("spreadsheets");
var scope = "https://spreadsheets.google.com/feeds"
oauthConfig.setConsumerKey("anonymous");
oauthConfig.setConsumerSecret("anonymous");
oauthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?scope="+scope);
oauthConfig.setAuthorizationUrl("https://accounts.google.com/OAuthAuthorizeToken");
oauthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
var requestData = {
"oAuthServiceName": "spreadsheets",
"oAuthUseToken": "always",
};
var name = DocsList.getFileById(key).getName()+".pdf";
var pdf = UrlFetchApp.fetch("https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key="+key+
"&exportFormat=pdf&gid=0&gridlines=true&printtitle=true&size=A4&sheetnames=true&fzr=true&portrait=true&fitw=true", requestData).getBlob().setName(name);
return pdf;
}
/*
fmcmd=12
size=legal/A4
fzr=true/false
portrait=false/true
fitw=true/false
gid=0/1/2
gridlines=false/true
printtitle=false/true
sheetnames=false/true
pagenum=UNDEFINED
attachment=false/true
*/

Google Appscript approvals for public document

I have a google spreadsheet that is shared with all members on our domain. I'd like to have a script which will send the spreadsheet as an email attachment whenever one of the users runs the script.
Following this question as a guide, I arrived at the following code:
function sendEmail() {
var ssID = SpreadsheetApp.getActiveSpreadsheet().getId();
var sheetName = SpreadsheetApp.getActiveSpreadsheet().getName();
//var email = Session.getUser().getEmail();
var email = "xxxxx#example.com";
var subject = "Order Form";
var body = "Please find the attached Order Form for drop-ship delivery.";
var oauthConfig = UrlFetchApp.addOAuthService("google");
oauthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oauthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?scope=https://spreadsheets.google.com/feeds/");
oauthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oauthConfig.setConsumerKey("anonymous");
oauthConfig.setConsumerSecret("anonymous");
var requestData = {"method": "GET", "oAuthServiceName": "google", "oAuthUseToken": "always"};
var url = "https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key="
+ ssID + "&gid=0&portrait=true" +"&exportFormat=xls";
var result = UrlFetchApp.fetch(url , requestData);
var contents = result.getContent();
MailApp.sendEmail(email,subject ,body, {attachments:[{fileName:sheetName+".xls", content:contents, mimeType:"application//xls"}]});
}
This code works, but only if I run the code from the script editor first (which involves authorizing access to the google mail account), and then authorizing the script when using the script.
So, I passed along the document to the order department, and again, it only works when EACH USER authorizes the script from the script editor, and then authorizes the script when using it.
Is there a way to eliminate the "authorizing by way of the script editor"? I'd really not like to have to go into each users account to authorize this script for them (as well as have to remember to do the same for any new user created)?
I appreciate any help offered!
Unfortunately NOT. see also my question and the answer by one of the google developer advocates.
You can vote for the issue I opened to push for a solution.

How to add a Google Drive folder to "My Drive" section to other users

I have a Google Drive herarchichal folder and subfolders structure owned by me and I want to add it to the "My Drive" section in all users in our Google Apps for Business domain automatically.
How can I do this? I heard about Google Apps Script and AddToFolder function.
Please, can you help me?
Thanks in advance.
This is very easy to do if each user could just access a link and authorize a script (that you build) to do the job for them (place a shared folder in their root folder).
But if it's a lot of users, you are the admin of the domain, and you really want to do it all automatically without anyone doing a thing, it is possible but probably very difficult to do. I mean, you need to access the Drive API directly and set oAuth 2.0 to impersonate your users, because the Apps Script built-in DocsList API does not have this impersonation feature. If you're really going for it, take a look at this other question.
First, set up a simple web app. The Google App Script editor even has a template that gets you most of the way there.
Second, implement something like the following and call it from the handler function.
function addRequiredFolders() {
var root = DocsList.getRootFolder();
var folderIds = ["somefolderid", "anotherfolderid"];
folderIds.map( function(id) { DocsList.getFolderById(id).addToFolder(root) } );
}
I've tested a variant of this up to this point. The next step is to publish the Web App for your domain, and email it out to people or otherwise distribute it. I assume they will have the unpleasant step of needing to grant the web app permission to access their documents.
Right now I've implemented this feature using "Google Documents List API". I know that this API is deprecated but for now it works.
(the code is not finished)
(...)
//var user = Session.getActiveUser().getUserLoginId() OR
var user = e.parameter.user_email
var TB_folder = e.parameter.TB_folder
var TB_sub_folder = e.parameter.TB_sub_folder
var base = 'https://docs.google.com/feeds/';
var fetchArgs = googleOAuth_('docs', base);
fetchArgs.method = 'POST';
var rawXml = "<?xml version='1.0' encoding='UTF-8'?>" + "<entry xmlns='http://www.w3.org/2005/Atom'>"
+ "<category scheme='http://schemas.google.com/g/2005#kind' "
+ "term='http://schemas.google.com/docs/2007#folder'/>"
+ "<title>" + TB_folder +"</title>"
+ "</entry>";
fetchArgs.payload = rawXml;
fetchArgs.contentType = 'application/atom+xml';
fetchArgs.contentLength = 245;
// POST a https://docs.google.com/feeds/default/private/full
var url = base + user + '/private/full/?v=3&alt=json';
var content = UrlFetchApp.fetch(url, fetchArgs).getContentText()
var json = Utilities.jsonParse(content)
var folder = json.entry.gd$resourceId.$t // -> I get "folder:folder_id"
var id_folder = folder.split(':')[1]
var folder_created_by = json.entry.gd$lastModifiedBy.email.$t
var folder_owner = json.entry.author['0'].email.$t
(...)
Now, you have the folder ID and can use it to create another subfolder or a file...
You need this function :
//Google oAuth
function googleOAuth_(name,scope) {
var oAuthConfig = UrlFetchApp.addOAuthService(name);
oAuthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?scope="+scope);
oAuthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oAuthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oAuthConfig.setConsumerKey("XXXXXXXX");
oAuthConfig.setConsumerSecret("XXXXXXXXXXXXXXXXXXX");
//oAuthConfig.setConsumerKey("anonymous");
//oAuthConfig.setConsumerSecret("anonymous");
return {oAuthServiceName:name, oAuthUseToken:"always"};
}
You can create a file or add a user to a file (add a wriker).
Now I want to implement this functionality with " Drive API " . If someone has done it would be nice to get some help.
Sergi