Publish a AppScript Project as a webApp from the same Script file - google-apps-script

I want to create a method, with which I can publish a appScript code as a webapp from the same script file.
Though there are REST api to do the same, I am trying to figure out a wat to do it though appScript it self.

You can deploy a web app from any script project, including a script project that is bound to a Docs Editor file such as a spreadsheet.
See the Web App Demo answer for more information and sample code.

Answer
There is no method within the ScriptApp class to deploy.
Work-around
You can make an API call to the projects.deployments.create method with the UrlFetchApp class. In order to make a successful call you must activate the Apps Script API.
var scriptId = 'some id'
var url = `https://script.googleapis.com/v1/projects/${scriptId}/deployments`
var formData = {
"versionNumber": 1,
"description": "DeployFromApi",
"manifestFileName": "appsscript"
}
var options = {
'method' : 'post',
'payload' : formData
};
UrlFetchApp.fetch('https://httpbin.org/post', options);

Related

OAuth problem with Apps Script and Google Chat

I'm currently trying to make a google chat bot on Apps Script, using webhooks that supposedly don't require any authentification.
However, when I run my Apps Script function from Apps Script, it seems like I have to allow my account to "use external web apps" or something along these lines.
I developped the Apps Script as an API, so that I could call the functions from an external point, but it says that I need OAuth credentials to do so. And I know how to implement OAuth credentials, I just have no idea what scope I'm supposed to use since webhooks are supposed to work without authentification.
Here's the function:
function sendText(text) {
var url = "https://chat.googleapis.com/v1/spaces/[space]/messages?key=[webhook-key]&token=[token]";
message = { 'text': text };
var message_headers = {};
const response = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(message),
});
Logger.log(response);
}
okay I found the answer myself;
scope is : https://googleapis.com/auth/script.external_request

Google Sheets Advanced Google Services URL Shortener 403 Error: Forbidden

I am trying to create a small application in in Google Sheets to sorten URLs on my personal google account. I am using the following code which I found here: Google Sheets Function to get a shortened URL (from Bit.ly or goo.gl etc.)
function onOpen() {
SpreadsheetApp.getUi()
.createMenu("Shorten")
.addItem("Go !!","rangeShort")
.addToUi()
}
function rangeShort() {
var range = SpreadsheetApp.getActiveRange(), data = range.getValues();
var output = [];
for(var i = 0, iLen = data.length; i < iLen; i++) {
//var url = UrlShortener.Url.insert({longUrl: data[i][0]});
var url = UrlShortener.Url.insert({longUrl: 'www.google.com'});
output.push([url.id]);
}
range.offset(0,1).setValues(output);
}
I created a new Google Cloud Project and enabled the URL shortener API in the project and on the Google sheet. The problem is that when I try and run the code I get an err on the line: var url = UrlShortener.Url.insert({longUrl: 'www.google.com'});
error 403, message:forbidden
when i try an execute the rangeShort() function. I have no idea how to fix this. Any ideas would be most appreciated! Thanks!
As it turns out, like Ruben mentioned, Google has moved away from their URL shortener. So after much research ans testing here is the solution:
Step 1
Migrate Google Cloud Project over to Firebase or create a new Firebase Project. See steps here
Step 2
Create a dummy project in order to create a base URL for the shortening. See this youtube video
Step 3
Get the Web API Key from your new Firebase Project (not the app you just created)
Step 4
Check the left side menu on the screen and navigate to Grow->Dynamic Links. You should see the new application you created and a URL at the top of the application. This will become the base of the new shortened URLs.
Step 5
Create the code in Google Apps Script inside the code builder from within Google Sheets. Here is the code that worked for me (I passed the url into this function) (This code is based on the answer found here):
function api_call(url){
var req='https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=[YOUR PROJECT WEB API KEY FROM STEP 3]';
var formData = {
"longDynamicLink": "[YOUR APPLICATION URL BASE FROM STEP 4]?link=" + url,
"suffix" : {
"option" : "UNGUESSABLE"
}
};
var options = {
'method': 'post',
'contentType': 'application/json',
'payload': JSON.stringify(formData)
};
var response = UrlFetchApp.fetch(req, options);
var res=JSON.parse(response);
return res.shortLink;
}
Additional Information
Documentation on Creating Dynamic Links in Firebase
Documentation on using UrlFetchApp() in Google Apps Script
If the url shortener service was used in your project before March 30,2018
Instead of
www.google.com
use
https://www.google.com
Reference: https://developers.google.com/url-shortener/v1/url/insert
but if your project was created on or after March 30, 2018
From https://developers.google.com/url-shortener/v1/
Starting March 30, 2018, we will be turning down support for goo.gl URL shortener. Please see this blog post for detailed timelines and alternatives.
Just to be clear, please note, from the linked blog post:
For developers
Starting May 30, 2018, only projects that have accessed URL Shortener
APIs before today can create short links.
I can attest to #alutz's answer here with a small addition/correction to their code.
Use encodeURIcomponent() for the input url while assigning it to the Long Dynamic Link in case you have more than one custom parameters.
"longDynamicLink": "[YOUR APPLICATION URL BASE FROM STEP 4]?link=" + encodeURIcomponent(url),
This allowed me to pass in multiple arguments for my telegram bot like chat_id, text and parse_mode.

Update (Overwrite) one Apps Script file with another Apps Script file using Apps Script Code

Overwrite file. Overwrite Apps Script file.
This is not a question to create a new Apps Script file. That won't help me. I need to update an existing Apps Script file. This question is similar to creating a new file, but it's not the same issue. The syntax for an update, and the requirements for an update may be different enough from creating a new file, that I can't get a solution from the answer about creating a new file. I've looked at an answer for creating a new file, and that has not answered my question.
I have tried using the Advanced Drive Service to update an existing Apps Script file with another Apps Script file.
function updateMyScript() {
var targetFileID = 'File ID';
var dataSource = {
"files": [
{
"id":"739a57da-f77c-4e1a-96df-7d86ef227d62",
"name":"Code",
"type":"server_js",
"source":"function doGet() {\n return HtmlService.createHtmlOutputFromFile(\u0027index\u0027);\n}\n"
},
{
"id":"2c7e8b5a-dbc5-4cd2-80e9-77b6291b3167",
"name":"index",
"type":"html",
"source":"\u003chtml\u003e\n \u003cbody\u003e\n New message!!\n \u003c/body\u003e\n\u003c/html\u003e"
}
]
};
var filesResource = Drive.Files.get(targetFileID);
var blob = Utilities.newBlob(JSON.stringify(dataSource), "application/vnd.google-apps.script+json");
var whatRezult = Drive.Files.update(filesResource, targetFileID, blob, {"convert":"true"});
Logger.log('whatRezult: ' + whatRezult);
};
The id properties in the dataSource object are id's of each specific .gs or html file inside of the project. I got those id's by downloading the "target" Apps Script file to JSON, then opening up the JSON file and copying out the file id's. So, I know that those are correct. I'd like a way to retrieve those individual file ID's from the project with code. The post about creating a new apps script file, does not explain how to get the "sub" ID's from the project file. The project file ID is not the same as each file ID inside of the project. The name and type properties are obvious. And there are only two types of files which, from the example, have types of server_js and "html". It looks like the source for the internal files to the Apps Script project file can be a string literal. So, you can just type in whatever you want the replacement content to be.
The target ID is self explanatory.
If there is a way to do this with either the Advanced Drive Service or UrlFetchApp.fetch() that will answer my question. I only want to use Apps Script. So, I'm not looking for a solution written in some other language, or that is run from outside of Apps Script.
With the above code, I'm getting an error from the next to last line:
Drive.Files.update(filesResource, targetFileID, blob, {"convert":"true"});
The error is:
The app does not have the required scope to update Apps Scripts.
So, obviously, it's looking for a scope setting to be indicated somewhere. I'm guessing that it would go into the the fourth parameter for the options.
You need to ask for the special Drive-AppsScript scope:
https://www.googleapis.com/auth/drive.scripts
Since you cannot tell Apps Script to ask this scope for you (it determines its own scopes automatically by analyzing your code). You need to do the oAuth dance yourself (by using Eric's lib for example). But then since you also cannot set this token you retrieved yourself for the script to use in its built-in or advanced service calls (not that I know of anyway), you'll have to do the UrlFetch call manually too, and pass your "custom" token in the header (like shown in the "create new script" question.
The UrlFetch call to update is very similar to the insert one. Just change the method to PUT and add the apps script project id in the path.
var url = "https://www.googleapis.com/upload/drive/v2/files/" + scriptID;
var requestBody = ...; //the same
var options = {
"headers": {
'Authorization': 'Bearer ' + yourManuallyFetchedToken,
},
"contentType": "application/vnd.google-apps.script+json",
"method" : "PUT", //changed here from POST to PUT
"payload": JSON.stringify(requestBody)
}
I've created a GitHub repository of a project that will update one Apps Script file (the target) from a source Apps Script file. It's totally free to copy and use.
GitHub Repository - apps-script-update
See the Read Me file in the GitHub repository for instructions
The code at GitHub is using the newer Apps Script API, which is different from the original answer.
The new Apps Script API now allows for an Apps Script project to be updated. The project that is updated can be bound to a document. (Sheet, Form, Doc) This code uses the REST API, and makes a PUT request from Apps Script code using UrlFetchApp.fetch(url,options) This code is being run from an Apps Script file to update another Apps Script file.
The Apps Script API must be enabled for the Apps Script file running the code. The Apps Script API is enabled in the Google Cloud console. From the code editor, choose "Resources" and "Cloud Platform project" Search for Apps Script API and enable it.
function updateContent(scriptId,content,theAccessTkn) {
//try{
var options,payload,response,url;
if (!content) {
//Error handling function
return;
}
if (!theAccessTkn) {
theAccessTkn = ScriptApp.getOAuthToken();
}
//https://developers.google.com/apps-script/api/reference/rest/v1/projects/updateContent
url = "https://script.googleapis.com/v1/projects/" + scriptId + "/content";
options = {
"method" : "PUT",
"muteHttpExceptions": true,
"headers": {
'Authorization': 'Bearer ' + theAccessTkn
},
"contentType": "application/json",//If the content type is set then you can stringify the payload
"payload": JSON.stringify(content)
};
response = UrlFetchApp.fetch(url,options);
response = JSON.parse(response);//Must be parsed even though it shows as coming back as an object
//Logger.log('typeof response: ' + typeof response)
//Logger.log('response 29 in file GS_Update: ' + JSON.stringify(response).slice(0,45))
return response;
//} catch(e) {
//Logger.log(response)
//}
};
You must use the correct scopes in order for the code to run without an authorization error.
The scopes can be set in the appsscript.json file. To view the appsscript.json file, you must first click the View menu, and choose the "Show manifest" menu item.
{
"timeZone": "America/New_York",
"oauthScopes": [
"https://www.googleapis.com/auth/script.projects",
"https://www.googleapis.com/auth/script.external_request"
],
"dependencies": {
},
"exceptionLogging": "STACKDRIVER"
}
The first time that the Apps Script API is used, the PUT request may not work, and will return an error with a link to the Google Cloud console. That's why it's important to view the return response Logger.log('typeof response: ' + typeof response) from the response = UrlFetchApp.fetch(url,options); statement.

Calling a Google Apps Script web app with access token

I need to execute a GAS service on behalf of a user that is logged to my system. So I have her/his access token. I would like somehow to transfer the token to the web app and without having to authorize again the user to use it for some activities. Can this be accomplished? Thank you.
EDIT: I think I didn't explain right what I try to accomplish. Here is the work flow I try to achieve:
We authorize a user visiting our website using OAuth2 and Google;
We get hold of her/his access token that Google returns;
There is a Google Apps Script web app that is executed as the user running the web app;
We want to call this app (3) by providing the access token (2) so Google not to ask again for authorization;
Actually, we want to call this app (3) not by redirecting the user to it but by calling it as a web service.
Thanks
Martin's answer worked for me in the end, but when I was making a prototype there was a major hurdle.
I needed to add the following scope manually, as the "automatic scope detection system" of google apps script did not ask for it: "https://www.googleapis.com/auth/drive.readonly". This resulted in UrlFetchApp.fetch always giving 401 with additional information I did not understand. Logging this additional information would show html, including the following string
Sorry, unable to open the file at this time.</p><p> Please check the address and try again.
I still don't really understand why "https://www.googleapis.com/auth/drive.readonly" would be necessary. It may have to do with the fact that we can use the /dev url, but who may use the /dev url is managed is checked using the drive permissions of the script file.
That said, the following setup then works for me (it also works with doGet etc, but I chose doPost). I chose to list the minimally needed scopes explicitly in the manifest file, but you can also make sure the calling script will ask for permissions to access drive in different ways. We have two google apps script projects, Caller and WebApp.
In the manifest file of Caller, i.e. appsscript.json
{
...
"oauthScopes":
[
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/script.external_request"]
}
In Code.gs of Caller
function controlCallSimpleService(){
var webAppUrl ='https://script.google.com/a/DOMAIN/macros/s/id123123123/exec';
// var webAppUrl =
// 'https://script.google.com/a/DOMAIN/macros/s/id1212121212/dev'
var token = ScriptApp.getOAuthToken();
var options = {
'method' : 'post'
, 'headers': {'Authorization': 'Bearer '+ token}
, muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(webAppUrl, options);
Logger.log(response.getContentText());
}
In Code.gs of WebApp (the web app being called)
function doPost(event){
return ContentService.createTextOutput("Hello World");
}
The hard answer is NO you can't use the built-in services of Apps Script with a service token. But if you already have the token for a user generated by a service account, access to the users data is pretty similar to any other language. All calls would be to the REST interface of the service your token is scoped for.
Take this small script for example. It will build a list of all the user's folders and return them as JSON:
function doGet(e){
var token = e.parameter.token;
var folderArray = [];
var pageToken = "";
var query = encodeURIComponent("mimeType = 'application/vnd.google-apps.folder'");
var params = {method:"GET",
contentType:'application/json',
headers:{Authorization:"Bearer "+token},
muteHttpExceptions:true
};
var url = "https://www.googleapis.com/drive/v2/files?q="+query;
do{
var results = UrlFetchApp.fetch(url,params);
if(results.getResponseCode() != 200){
Logger.log(results);
break;
}
var folders = JSON.parse(results.getContentText());
url = "https://www.googleapis.com/drive/v2/files?q="+query;
for(var i in folders.items){
folderArray.push({"name":folders.items[i].title, "id":folders.items[i].id})
}
pageToken = folders.nextPageToken;
url += "&pageToken="+encodeURIComponent(pageToken);
}while(pageToken != undefined)
var folderObj = {};
folderObj["folders"] = folderArray;
return ContentService.createTextOutput(JSON.stringify(folderObj)).setMimeType(ContentService.MimeType.JSON);
}
You do miss out on a lot of the convenience that makes Apps Script so powerful, mainly the built in services, but all functionality is available through the Google REST APIs.
I found a way! Just include the following header in the request:
Authorization: Bearer <user's_access_token>

Google Apps Script - Handling Result from UrlFetchApp.fetch()

I have a spreadsheet that I only want users to modify by running a script. The script is a UiApp that has a few pre-defined input fields and text boxes and the results are submitted onto the spreadsheet. Because I only want the document modified from this app, I have to set the permissions of the spreadsheet to "Can comment." However, in doing this, the users cannot run the script (because the script edits the page and they don't have editing rights to the page). So I assume that I need to create a web app.
The web app would be stand-alone and would run as me (the owner) so that calls to the app would allow the submitted data to be written to the spreadsheet. My web app looks something like this:
function doGet() {
var app = UiApp.createApplication();
// UiApp elements are added here
return app;
}
...and the works fine when the url is accessed directly from the browser. However, I would like for the app to open w/i the spreadsheet from a spreadsheet trigger. I was thinking something like this:
var app = UrlFetchApp.fetch(url);
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.show(app);
...but this is not working. The error I get is: "Invalid argument: userInterface (line 12, file "Web App")." Line 12 is "ss.show(app)." I was hoping that the app object would be returned from UrlFetch, but I now know that an HTTPResponse is returned.
How can I convert this response into a UiApp object? Thanks.
The solution I came up with was to have the UiApp open on the spreadsheet (from a trigger) and have the user choose the drop-downs and complete the text boxes. Upon clicking the submit button, the handler function would take all of the parameters and create a payload. Then this payload was passed to a doPost(e) stand-alone web app. Because I passed the ssid, the web app was able to locate the spreadsheet/sheet/range and write/format the data in a certain way. Here is my code:
var payload = {
"ssid" : ssid,
"sheetName" : sheetName,
"row" : row,
"col" : col,
"method" : method,
"strategy" : strategy,
"summary" : summary,
};
var options = {
"method" : "post",
"payload" : payload
};
UrlFetchApp.fetch(url, options);
This way the users can input the information in a certain format without having editing rights to the sheet.