Cannot draft a reply. Permission error - gmail-addons

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.

Related

Google add on : Show a notification with the first loaded card

Google add on initialization works with a function being called, the function is defined in the appscript JSON :
"addOns": {
"common": {
"name": "name of add on",
"homepageTrigger": {
"runFunction": "onLoad",
"enabled": true
},
The onLoad function has to return a built Card (CardService.newCardBuilder().build()).
I would like to make it so that the initial card that is build also sends a notification (the temporary message that shows as a snackbar for a few seconds on the add-on).
This would be done by adding a notification to an action response and building it.
var card = buildCard();
var navigation = CardService.newNavigation().popCard().pushCard(card);
var actionresponse = CardService.newActionResponseBuilder()
.setNavigation(navigation)
.setNotification(CardService.newNotification()
.setText(notificationText)
.setType(CardService.NotificationType.INFO));
return actionresponse.build();
However, the onLoad function does not accept a built action response, it needs a built card.
Any way to overcome this ?
Thank you

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);
}

My GSM Gmail addon does not show the card

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.

ReferenceError: ConferenceDataService is not defined

I am trying to develop google calendar add-on like zoom meeting.
In appsscript.json file, below code is there.
"calendar": {
"conferenceSolution": [{
"onCreateFunction": "createConference",
"id": "1",
"name": "Meeting",
"logoUrl": "https://companyxyz.com/images/logo192.png"
}],
"eventOpenTrigger": {
"runFunction": "buildSimpleCard"
},
"currentEventAccess": "READ_WRITE"
}
}
In Calendar.gs, below code is there.
function createConference(e) {
Logger.log(e);
var dataBuilder = ConferenceDataService.newConferenceDataBuilder();
return dataBuilder.build();
}
/**
* Build a simple card with a button that sends a notification.
* This function is called as part of the eventOpenTrigger that builds
* a UI when the user opens a Calendar event.
*
* #param e The event object passed to eventOpenTrigger function.
* #return {Card}
*/
function buildSimpleCard() {
var buttonAction = CardService.newAction()
.setFunctionName('onSaveConferenceOptionsButtonClicked')
.setParameters(
{'phone': "1555123467", 'adminEmail': "joyce#example.com"});
var button = CardService.newTextButton()
.setText('Add new attendee')
.setOnClickAction(buttonAction);
var buttonSet = CardService.newButtonSet()
.addButton(button);
var section = CardService.newCardSection()
.setHeader("addon")
.addWidget(buttonSet);
var card = CardService.newCardBuilder()
.addSection(section)
//.setFixedFooter(footer);
return card.build();
// Check the event object to determine if the user can set
// conference data and disable the button if not.
// if (!e.calendar.capabilities.canSetConferenceData) {
// button.setDisabled(true);
// }
// ...continue creating card sections and widgets, then create a Card
// object to add them to. Return the built Card object.
}
/**
* Callback function for a button action. Sets conference data for the
* Calendar event being edited.
*
* #param {Object} e The action event object.
* #return {CalendarEventActionResponse}
*/
function onSaveConferenceOptionsButtonClicked(e) {
var parameters = e.commonEventObject.parameters;
// Create an entry point and a conference parameter.
var phoneEntryPoint = ConferenceDataService.newEntryPoint()
.setEntryPointType(ConferenceDataService.EntryPointType.PHONE)
.setUri('tel:' + parameters['phone']);
var adminEmailParameter = ConferenceDataService.newConferenceParameter()
.setKey('adminEmail')
.setValue(parameters['adminEmail']);
// Create a conference data object to set to this Calendar event.
var conferenceData = ConferenceDataService.newConferenceDataBuilder()
.addEntryPoint(phoneEntryPoint)
.addConferenceParameter(adminEmailParameter)
.setConferenceSolutionId(1)
.build();
return CardService.newCalendarEventActionResponseBuilder()
.setConferenceData(conferenceData)
.build();
}
I have published this add-on from Publish->Deploy from menifest.
Executing this code giving me error of ReferenceError: ConferenceDataService is not defined.
I have searched all the possible references, but not able to get any solution.
Please suggest me proper solution for this.
According to this comment from this issue here, it looks like there has been a change regarding this.
When testing the above code, the ReferenceError: ConferenceDataService is not defined. is not displayed anymore and the code runs as expected.
For other methods specific to the ConferenceDataService you can check the documentation here.
Reference
Apps Script ConferenceDataService.
Cannot create conference with 3rd party Conference Solution from conference solutions Dropdown in Google Calendar

Chrome Extension: how to capture selected text and send to a web service

For the Google Chrome extension, I need to capture selected text in a web page and send to a web service. I'm stuck!
First I tried a bookmarklet, but Chrome on Mac seems to have some bookmarklet bugs so I decided to write an extension.
I use this code in my ext:
function getSelText(){
var txt = 'nothing';
if (window.getSelection){
txt = "1" + window.getSelection();
} else if (document.getSelection) {
txt = "2" + document.getSelection();
} else if (document.selection) {
txt = "3" + document.selection.createRange().text;
} else txt = "wtf";
return txt;
}
var selection = getSelText();
alert("selection = " + selection);
When I click on my extension icon, I get a "1". So I think the act of selecting outside the browser window is causing the text to not be seen by the browser as "selected" any more.
Just a theory....
thoughts?
You can do this by using Extensions Messaging. Basically, your "background page" will send the request to your service. For example, lets say you have a "popup" and once you click on it, it will do a "Google search" which is your service.
content_script.js
In your content script, we need to listen for a request coming from your extension, so that we send it the selected text:
chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
if (request.method == "getSelection")
sendResponse({data: window.getSelection().toString()});
else
sendResponse({}); // snub them.
});
background.html
Now in background page you can handle the popup onclick event so that we know we clicked on the popup. Once we clicked on it, the callback fires, and then we can send a request to the content script using "Messaging" to fetch the selected text.
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.sendRequest(tab.id, {method: "getSelection"}, function(response){
sendServiceRequest(response.data);
});
});
function sendServiceRequest(selectedText) {
var serviceCall = 'http://www.google.com/search?q=' + selectedText;
chrome.tabs.create({url: serviceCall});
}
As you have seen, I registered a listener in a content script to allow my extension to send and receive messages from it. Then once I received a message, I handle it by searching for Google.
Hopefully, you can use what I explained above and apply it to your scenario. I just have to warn you that the code written above is not tested, so their might be spelling, or syntax errors. But those can easily be found by looking at your Inspector :)
content script
document.addEventListener('mouseup',function(event)
{
var sel = window.getSelection().toString();
if(sel.length)
chrome.extension.sendRequest({'message':'setText','data': sel},function(response){})
})
Background Page
<script>
var seltext = null;
chrome.extension.onRequest.addListener(function(request, sender, sendResponse)
{
switch(request.message)
{
case 'setText':
window.seltext = request.data
break;
default:
sendResponse({data: 'Invalid arguments'});
break;
}
});
function savetext(info,tab)
{
var jax = new XMLHttpRequest();
jax.open("POST","http://localhost/text/");
jax.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
jax.send("text="+seltext);
jax.onreadystatechange = function() { if(jax.readyState==4) { alert(jax.responseText); }}
}
var contexts = ["selection"];
for (var i = 0; i < contexts.length; i++)
{
var context = contexts[i];
chrome.contextMenus.create({"title": "Send to Server", "contexts":[context], "onclick": savetext});
}
</script>
manifest.json
{
"name": "Word Reminder",
"version": "1.0",
"description": "Word Reminder.",
"browser_action": {
"default_icon": "images/stick-man1.gif",
"popup":"popup.html"
},
"background_page": "background.html",
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["js/myscript.js"]
}
],
"permissions": [
"http://*/*",
"https://*/*",
"contextMenus",
"tabs"
]
}
and here is the link where i have all in one extension to download.
after reading this i tried of my own and have published.
and here is the complete source
http://vikku.info/programming/chrome-extension/get-selected-text-send-to-web-server-in-chrome-extension-communicate-between-content-script-and-background-page.htm
Enjoy
Using a content_scripts is not a great solution as it injection to all documents including iframe-ads etc. I get an empty text selection from other pages than the one I expect half the times on messy web sites.
A better solution is to inject code into the selected tab only, as this is where your selected text lives anyhow. Example of jquery doc ready section:
$(document).ready(function() {
// set up an event listener that triggers when chrome.extension.sendRequest is fired.
chrome.extension.onRequest.addListener(
function(request, sender, sendResponse) {
// text selection is stored in request.selection
$('#text').val( request.selection );
});
// inject javascript into DOM of selected window and tab.
// injected code send a message (with selected text) back to the plugin using chrome.extension.sendRequest
chrome.tabs.executeScript(null, {code: "chrome.extension.sendRequest({selection: window.getSelection().toString() });"});
});
It is not clear from your code where it is. What I mean, is that if this code is either in popup html or background html then the results you are seeing are correct, nothing in those windows will be selected.
You will need to place this code in a content script so that it has access to the DOM of the page, and then when you click your browser action, you will need to send a message to the content script to fetch the current document selection.
You don't need a Google API for something as simple as this...
I'll use the Bing online service as an example. Note that the URL is set up to accept a parameter:
var WebService='http://www.bing.com/translator/?text=';
frameID.contentWindow.document.body.addEventListener('contextmenu',function(e){
T=frameID.contentWindow.getSelection().toString();
if(T!==''){e.preventDefault(); Open_New_Tab(WebService+encodeURIComponent(T)); return false;}
},false);
NB: The function "Open_New_Tab()" used above is an imaginary one that accepts the webservice URL with the encoded selected text as a parameter.
That's the idea basically.