My GSM Gmail addon does not show the card - google-apps-script

I have built a GSM add on and published it for my domain. I built the code, on Google Apps Script and set it up in Google API Console. I installed it for my domain, but it does not show the card in Gmail It should show in the sidebar, and in the compose window. It works fine when I install the head version from within google apps script, but then I publish it to GSM for users in my organization it doesn't work. The purpose of the addon is to collect information from fields in a card and use that in an email template. I think it might be something to do with OAuth scopes but I am not sure. This is my very first GSM project and I don't know what OAuth scopes I should declare in my code and in the Google API console.
Here is my code, I have 2 files, appscript.json, and code.gs.
code.gs:
function onGmailCompose(e) {
console.log(e);
var header = CardService.newCardHeader()
.setTitle('Use Template')
.setSubtitle('Use the template for sending an email after a review has been published.');
// Create text input for entering the cat's message.
var input2 = CardService.newTextInput()
.setFieldName('FName')
.setTitle('First Name')
.setHint('What is the readers first name?');
var input3 = CardService.newTextInput()
.setFieldName('BookTitle')
.setTitle('Reviewed Book Title')
.setHint('What is the title of the book reviewed?');
var input4 = CardService.newTextInput()
.setFieldName('BookAuthor')
.setTitle('Reviewed Book Author')
.setHint('Who is the author of the book reviewed?');
// Create a button that inserts the cat image when pressed.
var action = CardService.newAction()
.setFunctionName('useTemplate');
var button = CardService.newTextButton()
.setText('Use Template')
.setOnClickAction(action)
.setTextButtonStyle(CardService.TextButtonStyle.FILLED);
var buttonSet = CardService.newButtonSet()
.addButton(button);
// Assemble the widgets and return the card.
var section = CardService.newCardSection()
.addWidget(input2)
.addWidget(input3)
.addWidget(input4)
.addWidget(buttonSet);
var card = CardService.newCardBuilder()
.setHeader(header)
.addSection(section);
return card.build();
}
function useTemplate(e) {
console.log(e);
var FName = e.formInput.FName;
var Title = e.formInput.BookTitle;
var Author = e.formInput.BookAuthor;
var now = new Date();
var htmlIntro = '<p>Hello, ';
var html2 = ' Thank you for writing a book review at Good Book Reviews on ';
var html3 = ' by ';
var html4 = '. You Review has been published to our site. Any personal information you included was NOT published, including first name, last name, age, and email address. Only info you wrote about the book was published. You can see it right here! If you need anything else, feel free to contact us at support#goodbookreviews.page or reply to this email to contact us. <br> Happy Reading,<br> The Book Review Team</p>';
var message = htmlIntro + FName + html2 + Title + html3 + Author + html4;
var response = CardService.newUpdateDraftActionResponseBuilder()
.setUpdateDraftBodyAction(CardService.newUpdateDraftBodyAction()
.addUpdateContent(message, CardService.ContentType.MUTABLE_HTML)
.setUpdateType(CardService.UpdateDraftBodyType.IN_PLACE_INSERT))
.build();
return response;
}
function onGmailMessage(e) {
console.log(e);
var header = CardService.newCardHeader()
.setTitle('Unavailable')
.setSubtitle('Open the compose window to use template');
var card = CardService.newCardBuilder()
.setHeader(header);
return card.build();
}
appscript.json:
{
"timeZone": "America/Chicago",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"oauthScopes": ["https://www.googleapis.com/auth/gmail.compose"],
"runtimeVersion": "V8",
"addOns": {
"common": {
"name": "Review Published Email Template",
"logoUrl": "https://goodbookreviews.page/Logo.png",
"useLocaleFromApp": true,
"universalActions": [{
"label": "Book Review ",
"openLink": "https://www.goodbookreviews.page"
}]
},
"gmail": {
"contextualTriggers": [{
"unconditional": {
},
"onTriggerFunction": "onGmailMessage"
}],
"composeTrigger": {
"selectActions": [{
"text": "Use Template",
"runFunction": "onGmailCompose"
}],
"draftAccess": "NONE"
}
}
}
}
The scopes that I specify in the OAuth consent screen page are here:
email
profile
openid
https://www.googleapis.com/auth/gmail.compose
The Email, profile, and openid are added by default and they are mandatory
Here are the scopes that I specify in the configuration page of the GSM SDK.
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/gmail.compose

You are very close to making this add-on works. You only need to add some scopes to your manifest file. Your final manifest should look similar to this one:
{
"timeZone":"America/Chicago",
"dependencies":{
},
"exceptionLogging":"STACKDRIVER",
"oauthScopes":[
"https://www.googleapis.com/auth/gmail.compose",
"https://www.googleapis.com/auth/gmail.addons.current.action.compose",
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/script.locale"
],
"runtimeVersion":"V8",
"addOns":{
"common":{
"name":"Review Published Email Template",
"logoUrl":"https://goodbookreviews.page/Logo.png",
"useLocaleFromApp":true,
"universalActions":[
{
"label":"Book Review ",
"openLink":"https://www.goodbookreviews.page"
}
]
},
"gmail":{
"contextualTriggers":[
{
"unconditional":{
},
"onTriggerFunction":"onGmailMessage"
}
],
"composeTrigger":{
"selectActions":[
{
"text":"Use Template",
"runFunction":"onGmailCompose"
}
],
"draftAccess":"NONE"
}
}
}
}
The rest of your code is correct. I deployed your add-on with this manifest, and it worked. Don't hesitate to ask any additional doubt if you need further clarification.

Related

How to insert Google Event with Extended Properties in GS?

I want to add / insert new event with extened properties in Google Script = .gs file.
I found code example for Calendar API - Events insert - see below. But the code uses the JavaScript client library. I want the code to run from GS file. I tried to modify but it did not work.
When using the code I want to be able to specify any calendar. Not only "primary".
// Refer to the JavaScript quickstart on how to setup the environment:
// https://developers.google.com/calendar/quickstart/js
// Change the scope to 'https://www.googleapis.com/auth/calendar' and delete any
// stored credentials.
var event = {
'summary': 'Google I/O 2015',
'location': '800 Howard St., San Francisco, CA 94103',
'description': 'A chance to hear more about Google\'s developer products.',
'start': {
'dateTime': '2015-05-28T09:00:00-07:00',
'timeZone': 'America/Los_Angeles'
},
'end': {
'dateTime': '2015-05-28T17:00:00-07:00',
'timeZone': 'America/Los_Angeles'
},
'recurrence': [
'RRULE:FREQ=DAILY;COUNT=2'
],
'attendees': [
{'email': 'lpage#example.com'},
{'email': 'sbrin#example.com'}
],
'reminders': {
'useDefault': false,
'overrides': [
{'method': 'email', 'minutes': 24 * 60},
{'method': 'popup', 'minutes': 10}
]
}
};
var request = gapi.client.calendar.events.insert({
'calendarId': 'primary',
'resource': event
});
request.execute(function(event) {
appendPre('Event created: ' + event.htmlLink);
});
Could someone please explain the difference between private and shared extended properties?
I am able to create new event using below code but looks like it will not store extended properties.
function getCalendar() {
var calendarId = 'processor#mydomain.com'
var calendar = CalendarApp.getCalendarById(calendarId)
Logger.log('The calendar is named "%s".', calendar.getName());
var eventOption = {
location: 'The Moon',
description: 'link na akci je https://us02web.zoom.us/j/83314336043',
extendedProperties: { // Extended properties of the event.
private: { // Properties that are private to the copy of the event that appears on this calendar.
creator: "Radek", // The name of the private property and the corresponding value.
},
}
}
var event = calendar.createEvent('test event from the script',
new Date(),
new Date(),
eventOption
);
var eventId = event.getId().replace(/#.*/,'') // // Remove #google.com from eventId
Logger.log('Event ID: ' + eventId)
calendarId = 'primary'
var eventSaved = Calendar.Events.get(encodeURIComponent(calendarId), eventId)
var testEx = event.extendedProperties
var test = event.extendedProperties.private["creator"];
}
Answer for question 1:
Could someone please explain the difference between private and shared extended properties?
The official document says as follows.
extendedProperties.private: Properties that are private to the copy of the event that appears on this calendar.
extendedProperties.shared: Properties that are shared between copies of the event on other attendees' calendars.
For example, when a new event is created with the values of extendedProperties.private and extendedProperties.shared by including the attendees, you can see both values. But, the attendees can see only the value of extendedProperties.shared.
Is this explanation useful?
Answer for question 2:
I want to add / insert new event with extened properties in Google Script = .gs file.
When I saw the official document of the method of createEvent(title, startTime, endTime, options), it seems that options has no property of extendedProperties. Ref I thought that this is the reason for your issue. If you want to create a new event including the values of extendedProperties.private and extendedProperties.shared, how about using Calendar API of Advanced Google services?
The sample script is as follows.
const calendarId = "###"; // Please set your calendar ID.
// Create a new event including extendedProperties.
const params = {
start: { dateTime: "2022-04-27T00:00:00Z" },
end: { dateTime: "2022-04-27T01:00:00Z" },
extendedProperties: {
private: { key1: "value1" },
shared: { key2: "value2" }
},
summary: "sample",
attendees: [{ email: "###" }] // Please set the email of attendee, if you want to include.
};
const res1 = Calendar.Events.insert(params, calendarId);
// Check the value of extendedProperties
const res2 = Calendar.Events.get(calendarId, res1.id);
console.log(res2.extendedProperties)
When this script is run by the owner of the calendar, you can see both values of extendedProperties.private and extendedProperties.shared.
When you get this event by the attendee, you can see only the value of extendedProperties.shared.
References:
createEvent(title, startTime, endTime, options)
Events: insert
Could someone please explain the difference between private and shared extended properties?
Based on documentation, shared extended properties are visible and editable by attendees while private set on one attendee's local "copy" of the event.
To add extended properties to events with Apps Script, you can do it with the advanced Calendar service. For this, you need to add the “Google Calendar API” service in your Apps Script project, on the left side of the screen, click on the “+” next to “Services”, search for “Google Calendar API”, click on it and click “Add”.
After completing the steps mentioned above, you can test this script I created as an example.
function createEvent() {
var calendarId = 'processor#mydomain.com' //you can specify the calendar with the calendar id
var start = new Date();
var end = new Date();
var event = {
"location": "The Moon",
"description": "link na akci je https://us02web.zoom.us/j/83314336043",
"start": {
"dateTime": start.toISOString(),
},
"end": {
"dateTime": end.toISOString()
},
"extendedProperties": {
"private": {
"creator": "Radek"
}
}
};
event = Calendar.Events.insert(event, calendarId);
Logger.log('Event ID: ' + event.id);
}

Create Classroom annoucement with Google script

Does exist a way to creare an Announcement in Google Classroom within Google App Script?
I've looked in the reference but only the REST way is documented.
In the editor it seems to exists a Classroom.newAnnouncement, but there's no documentation on what fields it requires and how to attach the created announcement to a course.
Any experience with that?
Here's the code.
function createAnnounce() {
var ClassSource = {
text: "STRINGS"+"\n"+"STRINGS"
};
Classroom.Courses.Announcements.create(ClassSource, COURSEID)
Logger.log(ClassSource);
}
The previous answer is right on the money.
you can also add a file to the announcement as is present in the updated code bellow:
function Announcement() {
var ClassSource = {
text: "STRINGS"+"\n"+"STRINGS",
materials: [
{
driveFile:{
driveFile: {
id: "FileID",
title: "Sample Document"
},
}
}
],
};
Classroom.Courses.Announcements.create(ClassSource, COURSEID)
Logger.log(ClassSource);
}

Cannot draft a reply. Permission error

I followed official guides https://developers.google.com/gmail/add-ons/how-tos/compose and https://developers.google.com/gmail/add-ons/guides/quickstart
Here is my appsscript.json:
{
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/gmail.addons.current.action.compose"
],
"gmail": {
"name": "Gmail Add-on Quickstart",
"logoUrl": "https://www.gstatic.com/images/icons/material/system/2x/bookmark_black_24dp.png",
"contextualTriggers": [{
"unconditional": {
},
"onTriggerFunction": "createReplyDraft"
}],
"openLinkUrlPrefixes": [
"https://mail.google.com/"
],
"primaryColor": "#4285F4",
"secondaryColor": "#4285F4"
}
}
and Code.gs as:
var composeAction = CardService.newAction()
.setFunctionName('createReplyDraft');
var composeButton = CardService.newTextButton()
.setText('Compose Reply')
.setComposeAction(composeAction, CardService.ComposedEmailType.REPLY_AS_DRAFT);
// ...
/**
* Creates a draft email (with an attachment and inline image)
* as a reply to an existing message.
* #param {Object} e data passed by the compose action.
* #return {ComposeActionResponse}
*/
function createReplyDraft(e) {
// Activate temporary Gmail add-on scopes, in this case to allow
// a reply to be drafted.
var accessToken = e.messageMetadata.accessToken;
GmailApp.setCurrentMessageAccessToken(accessToken);
// Creates a draft reply.
var messageId = e.messageMetadata.messageId;
var message = GmailApp.getMessageById(messageId);
var draft = message.createDraftReply('',
{
htmlBody: "Kitten!"
}
);
// Return a built draft response. This causes Gmail to present a
// compose window to the user, pre-filled with the content specified
// above.
return CardService.newComposeActionResponseBuilder()
.setGmailDraft(draft).build();
}
I want to open the "Reply" panel of my Gmail and paste some content into it.
It is giving Error following error: with the add-on.
Runtime error.
Access denied: : Cannot compose without user interaction.. [line: 27, function: createReplyDraft, file: Code]
I had already reinstalled module many times and also tried to give full scope permission of "https://mail.google.com/".
Looking at your code it looks like you are trying to call your CreateReplyDraft function as soon as the add-on is loaded. Google does not allow that, the user would have to click a button in the UI to track creating a draft.
Have you tried adding https://www.googleapis.com/auth/gmail.readonly to your scope?
You cannot trigger compose action directly like this. You have to create a button widget which has linked compose action(your createReplyDraft function) with it. So when the user clicks on that button, compose action will be triggered.

How upload a video to Youtube Using Google Apps Script?

I want to use Google Apps Script to upload a video YouTube using the YouTube Data API v3. This is my code:
function YouTubeAPI()
{
var url = {URL VIDEO};
var file = UrlFetchApp.fetch(url).getBlob();
Logger.log(file.getName());
var snippet = {
"snippet": {
"title": "Summer vacation in California",
"description": "Had a great time surfing in Santa Cruz",
"tags": ["surfing", "Santa Cruz"],
"categoryId": "22"},"status": {"privacyStatus": "private"}};
YouTube.Videos.insert(snippet, 'snippet,status', file)
}
The response is "Unauthorized", I check the Google Console is enable, and in the Script also enable the Youtube Data API.
If you have an MP4 file in your Google Drive that is less than 50MB, then you can get the file from your Google Drive and upload it to YouTube using the YouTube Data API. I created a new Google Cloud Platform (GCP) project, and enabled the YouTube Data API and the Google Drive API, and associated the new GCP project with my Apps Script project. Also, I manually added the needed scopes to the appsscript.json file.
{
"timeZone": "Your time zone will be here",
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "YouTube",
"version": "v3",
"serviceId": "youtube"
}
]
},
"oauthScopes": ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/script.external_request","https://www.googleapis.com/auth/youtube"],
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
The code:
function uploadToYouTube(mp4_fileId) {
var blob,mp4_fileId,part,requestResource,response;
var options = {},snippet = {};
/*
You will need to create a GCP standard project and associate it with this Apps Script project-
In the new code editor click the settings cog wheel and scroll down to:
Google Cloud Platform (GCP) Project -
You may get an error:
In order to change your project, you will need to configure the OAuth consent screen. Configure your OAuth Consent details.
And if you do not have a Google Workspace account then you wont be able to set up the GCP project as "INTERNAL"
You will need to enable the Google Drive API and the YouTube API in the associated GCP project -
*/
/*
This code needs the file ID of the MP4 file in your Google Drive -
To get the file ID of an MP4 video file in Google Drive, right click the MP4 in your Google Drive
and choose, "Get link"
The link will look like this:
https://drive.google.com/file/d/FILE_ID_IS_HERE/view?usp=sharing
In the URL is the file ID
*/
options = {
"method" : "get",
"headers" : {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
"muteHttpExceptions":true
}
mp4_fileId = mp4_fileId ? mp4_fileId : 'PUT_YOUR_MP4_FILE_ID_HERE';
const url = `https://www.googleapis.com/drive/v3/files/` + mp4_fileId + `?alt=media`;
response = UrlFetchApp.fetch(url, options);
//Logger.log('response.getResponseCode(): ' + response.getResponseCode())
if (response.getResponseCode() !== 200) {
return;
}
blob = response.getBlob();
//Logger.log('blob.getName(): ' + blob.getName())
/*
{"snippet":{
"playlistId":"YOUR_PLAYLIST_ID",
"position":0,
"resourceId":{
"kind":"youtube#video",
"videoId":"abcdefg"
}
}
}
*/
/*
{
"snippet": {
"title": "Summer vacation in California",
"description": "Had fun surfing in Santa Cruz",
"tags": ["surfing", "Santa Cruz"],
"categoryId": "22"
},
"status": {
"privacyStatus": "private"
}
}
*/
requestResource = {};
snippet.title = "AAA_Put_Title_Here";
snippet.description = "Description of video goes here";
snippet.categoryId = "22";
options.snippet = snippet;
options.status = {
"privacyStatus": "private"
}
part = "snippet,status";//This correlates to the options
//YouTube.Videos.insert(resource: Youtube_v3.Youtube.V3.Schema.Video, part: string[], mediaData: Blob, optionalArgs: Object)
var response = YouTube.Videos.insert(requestResource, part, blob, options);
if (!response || !response.kind) {//There was an error
console.log("Error!")
}
//Logger.log('response: ' + response);
}
Try to use code below. This sample code finds the user's uploads then updates the most recent upload's description by appending a string.
/**
* This sample finds the active user's uploads, then updates the most recent
* upload's description by appending a string.
*/
function updateVideo() {
// 1. Fetch all the channels owned by active user
var myChannels = YouTube.Channels.list('contentDetails', {mine: true});
// 2. Iterate through the channels and get the uploads playlist ID
for (var i = 0; i < myChannels.items.length; i++) {
var item = myChannels.items[i];
var uploadsPlaylistId = item.contentDetails.relatedPlaylists.uploads;
var playlistResponse = YouTube.PlaylistItems.list('snippet', {
playlistId: uploadsPlaylistId,
maxResults: 1
});
// Get the videoID of the first video in the list
var video = playlistResponse.items[0];
var originalDescription = video.snippet.description;
var updatedDescription = originalDescription + ' Description updated via Google Apps Script';
video.snippet.description = updatedDescription;
var resource = {
snippet: {
title: video.snippet.title,
description: updatedDescription,
categoryId: '22'
},
id: video.snippet.resourceId.videoId
};
YouTube.Videos.update(resource, 'id,snippet');
}
}
For more information, download the demo app here: https://github.com/youtube/api-samples

OAuth2 with Google Cloud Print

I had written a Google Apps Script that connected to Google Cloud Print to automate some printing. The script would auto-run on a time interval, search for relevant files, and if found it would sent them to my printer. My code used OAuthConfig and was working fine, but now that class has been deprecated and after a weekend of trial & error and scouring the interwebs I can't get it to work with OAuth2.
Here's the OAuthConfig code that was working fine:
function printDoc(docId, docTitle, myPrinterId) {
var scope = 'https://www.googleapis.com/auth/cloudprint';
var url = 'https://www.google.com/cloudprint/submit';
var payloadOfSubmit = {
"printerid" : myPrinterId,
"title" : docTitle,
"content" : docId,
"contentType" : "google.kix"
};
var fetchArgs = googleOAuth_('google', scope, payloadOfSubmit);
fetchArgs.method = 'POST';
var responseOfSubmit = UrlFetchApp.fetch(url, fetchArgs);
var jsonOfSubmit = JSON.parse(responseOfSubmit.getContentText());
return jsonOfSubmit;
}
function googleOAuth_(name, scope, payloadData) {
var oAuthConfig = UrlFetchApp.addOAuthService(name);
oAuthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oAuthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?scope="+scope);
oAuthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oAuthConfig.setConsumerKey("anonymous");
oAuthConfig.setConsumerSecret("anonymous");
return {
oAuthServiceName:name,
oAuthUseToken:"always",
muteHttpExceptions:true,
payload:payloadData
};
}
I've successfully connected the github library for OAuth2. However, what's different about the instructions provided there, and on many other sites, is that they assume that the code will be deployed as a web service where a user is prompted to manually click to authorize the request. In my case the code will be saved on a Google Apps Script file, and the Cloud Printer is on the same Google account, so I never needed this manual intervention or back & forth with my original OAuthconfig.
My first attempt by adapting the instructions was:
function printDoc2(docId, docTitle, myPrinterId) {
var url = 'https://www.google.com/cloudprint/submit';
var scope = 'https://www.googleapis.com/auth/cloudprint';
var payloadOfSubmit = {
"printerid" : myPrinterId,
"title" : docTitle,
"content" : docId,
"contentType" : "google.kix",
};
var accessToken = googleOAuth_('google', scope).getAccessToken();
var params = {
method:"POST",
headers: {"Authorization": "Bearer " + accessToken},
muteHttpExceptions:true,
payload:payloadOfSubmit
};
var responseOfSubmit = UrlFetchApp.fetch(url, params);
//Logger.log(responseOfSubmit);
var jsonOfSubmit = JSON.parse(responseOfSubmit.getContentText());
return jsonOfSubmit;
}
function googleOAuth2_(name, scope) {
return OAuth2.createService(name)
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId("anonymous")
.setClientSecret("anonymous")
.setProjectKey(ScriptApp.getProjectKey())
.setPropertyStore(PropertiesService.getUserProperties())
.setScope(scope)
.setCallbackFunction('authCallback');
}
function authCallback(request) {
var driveService = getDriveService();
var isAuthorized = driveService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
But this gives me an error "Access not granted or expired" when it tries to run the line:
var accessToken = googleOAuth_('google', scope).getAccessToken();
So I found a apps ScriptApp Method getOAuthToken which seemed like it might give me the token I need. I replaced the above line with:
var accessToken = ScriptApp.getOAuthToken();
And the code executes but my response from the server is "Error 403 User credentials required".
Here's my third attempt based on #Mogsdad's suggestion:
function sendPrintJob(docId,myPrinterId,docTitle) {
var payloadOfSubmit = {
"printerid" : myPrinterId,
"title" : docTitle,
"content" : docId,
"contentType" : "google.kix" ,
};
var request = {
"method": "POST",
"headers":{"Authorization": "Bearer "+ScriptApp.getOAuthToken()},
"muteHttpExceptions": true
};
var responseOfSubmit = UrlFetchApp.fetch("https://www.google.com/cloudprint/submit", request);
Logger.log(responseOfSubmit);
}
I've tried a number of variations, including creating a Developer Console Project and using the Client ID provided there, but I keep getting stuck at these two issues (access not granted, or credentials required). If anyone can provide any help I'd really appreciate it.
Here are the steps that allowed me to connect Google Apps Script to Google Cloud Print, so I could then submit GCP jobs (these steps are all started from within Google Apps Script):
Add the OAuth2 library
(https://github.com/googlesamples/apps-script-oauth2) to your Google
Apps Script by going to: Resources > Libraries > Find Library
MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48 > Select
Create new web application in Developer Console Resources > Developer Console Project > Click the project link > APIs & Auth >
Credentials > Add Credentials > OAuth2.0 Client ID > Web
Application > Set Authorized redirect URIs to the format
https://script.google.com/macros/d/{PROJECT KEY}/usercallback
where project key is under File > Project Properties and copy
your client ID and client secret
Add the ID and Secret to "getCloudPrintService()" code below (replace client_id and client_secret)
Go to Run > ShowURL and authorize the script.
Open the Logger (Cmd + Enter), copy the URL and paste it in a new browser tab to complete the authorization.
Go to https://www.google.com/cloudprint/#printers , select your printer, click details, expand advanced details, and copy your Printer ID (it will be of the format 555aa555-5a55-5555-5555-55555a55a555)
Add the printer id to "printGoogleDocument()" code below (replace myPrinterId)
This resource was helpful in figuring the steps out: http://ctrlq.org/code/20061-google-cloud-print-with-apps-script, and you may also find these links helpful:
https://developers.google.com/cloud-print/docs/appInterfaces
https://mashe.hawksey.info/2015/10/setting-up-oauth2-access-with-google-apps-script-blogger-api-example/
function showURL() {
var cpService = getCloudPrintService();
if (!cpService.hasAccess()) {
Logger.log(cpService.getAuthorizationUrl());
} else {
Logger.log("You already have access to this service.");
}
}
function getCloudPrintService() {
return OAuth2.createService('print')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId(client_id)
.setClientSecret(client_secret)
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('https://www.googleapis.com/auth/cloudprint')
.setParam('login_hint', Session.getActiveUser().getEmail())
.setParam('access_type', 'offline')
.setParam('approval_prompt', 'force');
}
function authCallback(request) {
var isAuthorized = getCloudPrintService().handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('You can now use Google Cloud Print from Apps Script.');
} else {
return HtmlService.createHtmlOutput('Cloud Print Error: Access Denied');
}
}
function getPrinterList() {
var response = UrlFetchApp.fetch('https://www.google.com/cloudprint/search', {
headers: {
Authorization: 'Bearer ' + getCloudPrintService().getAccessToken()
},
muteHttpExceptions: true
}).getContentText();
var printers = JSON.parse(response).printers;
for (var p in printers) {
Logger.log("%s %s %s", printers[p].id, printers[p].name, printers[p].description);
}
}
function printGoogleDocument(docId, docTitle) {
// For notes on ticket options see https://developers.google.com/cloud-print/docs/cdd?hl=en
var ticket = {
version: "1.0",
print: {
color: {
type: "STANDARD_COLOR"
},
duplex: {
type: "NO_DUPLEX"
},
}
};
var payload = {
"printerid" : myPrinterId,
"content" : docId,
"title" : docTitle,
"contentType" : "google.kix", // allows you to print google docs
"ticket" : JSON.stringify(ticket),
};
var response = UrlFetchApp.fetch('https://www.google.com/cloudprint/submit', {
method: "POST",
payload: payload,
headers: {
Authorization: 'Bearer ' + getCloudPrintService().getAccessToken()
},
"muteHttpExceptions": true
});
// If successful, should show a job here: https://www.google.com/cloudprint/#jobs
response = JSON.parse(response);
if (response.success) {
Logger.log("%s", response.message);
} else {
Logger.log("Error Code: %s %s", response.errorCode, response.message);
}
return response;
}
The scope "https://www.googleapis.com/auth/cloudprint" has to be included explicitly in the
manifest file
appscript.json (View > Show manifest file)
{
"timeZone": "Europe/Paris",
"dependencies": {
},
"oauthScopes": [
"https://www.googleapis.com/auth/documents",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/script.container.ui",
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/cloudprint"
],
"exceptionLogging": "STACKDRIVER"
}
Code.gs
function listPrinters() {
var options = {
headers: {
authorization: 'OAuth ' + ScriptApp.getOAuthToken()
}
}
var response = UrlFetchApp.fetch('https://www.google.com/cloudprint/search', options);
Logger.log(response);
}