1. Обзор
В этом практическом задании вы используете Google Apps Script для создания надстройки Google Workspace для Gmail, которая позволит пользователям добавлять данные чеков из электронных писем в электронную таблицу непосредственно в Gmail. Когда пользователь получает чек по электронной почте, он открывает надстройку, которая автоматически получает необходимую информацию о расходах из письма. Пользователь может редактировать информацию о расходах, а затем отправить ее для внесения данных о расходах в электронную таблицу Google Sheets.
Что вы узнаете
- Создайте надстройку Google Workspace для Gmail с помощью Google Apps Script.
- Анализ электронного письма с помощью Google Apps Script
- Взаимодействие с Google Таблицами через Google Apps Script
- Сохраняйте значения пользователей с помощью службы свойств Google Apps Script.
Что вам понадобится
- Доступ к интернету и веб-браузеру.
- Аккаунт Google
- Несколько сообщений, желательно подтверждений получения по электронной почте, в Gmail.
2. Получите пример кода.
В процессе выполнения этого практического задания вам может пригодиться рабочая версия кода, который вы будете писать. В репозитории GitHub содержится пример кода, который вы можете использовать в качестве образца.
Чтобы получить пример кода, выполните из командной строки:
git clone https://github.com/googleworkspace/gmail-add-on-codelab.git
3. Создайте базовое дополнение.
Начните с написания кода для простой версии дополнения, которое отображает форму для учета расходов вместе с адресом электронной почты.
Сначала создайте новый проект Apps Script и откройте его файл манифеста .
- Перейдите на сайт script.google.com . Здесь вы можете создавать, управлять и отслеживать свои проекты Apps Script.
- Чтобы создать новый проект, в левом верхнем углу нажмите «Новый проект» . Новый проект откроется с файлом по умолчанию под названием
Code.gsПока оставьтеCode.gsбез изменений, вы будете работать с ним позже. - Щелкните «Безымянный проект» , назовите свой проект «Expense It!» и нажмите «Переименовать» .
- Слева нажмите «Настройки проекта» .
. - Установите флажок «Показать файл манифеста "appscript.json" в редакторе» .
- Редактор кликов
. - Чтобы открыть файл манифеста, слева нажмите
appscript.json.
В appscript.json укажите метаданные, связанные с дополнением, такие как его имя и необходимые разрешения. Замените содержимое файла appsscript.json следующими параметрами конфигурации:
{
"timeZone": "GMT",
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute"
],
"gmail": {
"name": "Expense It!",
"logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/receipt_black_24dp.png",
"contextualTriggers": [{
"unconditional": {
},
"onTriggerFunction": "getContextualAddOn"
}],
"primaryColor": "#41f470",
"secondaryColor": "#94f441"
}
}
Обратите особое внимание на раздел манифеста, называемый contextualTriggers . Эта часть манифеста определяет определяемую пользователем функцию, которая вызывается при первой активации дополнения. В данном случае она вызывает getContextualAddOn , которая получает подробную информацию об открытом электронном письме и возвращает набор карточек для отображения пользователю.
Для создания функции getContextualAddOn выполните следующие действия:
- Слева наведите указатель мыши на
Code.gs, затем нажмите «Меню».
> Переименовать . - Введите
GetContextualAddOnи нажмите клавишуEnter. Apps Script автоматически добавит расширение.gsк имени файла, поэтому вам не нужно указывать расширение вручную. Если вы введетеGetContextualAddOn.gs, то Apps Script назовет ваш файлGetContextualAddOn.gs.gs. - В файле
GetContextualAddOn.gsзамените стандартный код функциейgetContextualAddOn:
/**
* Returns the contextual add-on data that should be rendered for
* the current e-mail thread. This function satisfies the requirements of
* an 'onTriggerFunction' and is specified in the add-on's manifest.
*
* @param {Object} event Event containing the message ID and other context.
* @returns {Card[]}
*/
function getContextualAddOn(event) {
var card = CardService.newCardBuilder();
card.setHeader(CardService.newCardHeader().setTitle('Log Your Expense'));
var section = CardService.newCardSection();
section.addWidget(CardService.newTextInput()
.setFieldName('Date')
.setTitle('Date'));
section.addWidget(CardService.newTextInput()
.setFieldName('Amount')
.setTitle('Amount'));
section.addWidget(CardService.newTextInput()
.setFieldName('Description')
.setTitle('Description'));
section.addWidget(CardService.newTextInput()
.setFieldName('Spreadsheet URL')
.setTitle('Spreadsheet URL'));
card.addSection(section);
return [card.build()];
}
Пользовательский интерфейс каждого дополнения Google Workspace состоит из карточек, разделенных на один или несколько разделов, каждый из которых содержит виджеты , позволяющие отображать информацию от пользователя и получать от него данные. Функция getContextualAddOn создает одну карточку, которая получает подробную информацию о расходах, найденных в электронном письме. Карточка имеет один раздел, содержащий текстовые поля для ввода соответствующих данных. Функция возвращает массив карточек дополнения. В данном случае возвращаемый массив содержит только одну карточку.
Перед развертыванием дополнения Expense It! вам потребуется проект Google Cloud Platform (GCP) , который проекты Apps Script используют для управления авторизацией, расширенными сервисами и другими деталями. Для получения дополнительной информации посетите страницу проектов Google Cloud Platform .
Для развертывания и запуска дополнения выполните следующие действия:
- Откройте свой проект GCP и скопируйте его номер .
- В проекте Apps Script слева нажмите «Настройки проекта» .
. - В разделе "Проект Google Cloud Platform (GCP)" нажмите " Изменить проект ".
- Введите номер проекта вашего GCP, затем нажмите « Установить проект» .
- Нажмите «Развернуть» > «Проверить развертывания» .
- Убедитесь, что тип развертывания — «Дополнение Google Workspace» . При необходимости в верхней части диалогового окна нажмите «Включить типы развертывания».
и выберите «Дополнение Google Workspace» в качестве типа развертывания. - Рядом с пунктом «Приложение(я): Gmail» нажмите «Установить» .
- Нажмите «Готово» .
Теперь вы можете увидеть это дополнение в своей почте Gmail.
- Откройте Gmail на своем компьютере.
- На правой боковой панели находится раздел «Расходы!».
Появляется дополнение. Возможно, вам потребуется нажать «Дополнительные дополнения».
найти его. - Откройте электронное письмо, желательно с квитанцией об оплате расходов.
- Чтобы открыть дополнение, в правой боковой панели нажмите «Expense It!».
. - Предоставьте Expense It! доступ к вашему аккаунту Google, нажав «Авторизовать доступ» и следуя инструкциям.
Дополнение отображает простую форму рядом с открытым сообщением Gmail. Пока оно ничего больше не делает, но вы расширите его функциональность в следующем разделе.
Чтобы отслеживать обновления вашего дополнения по мере прохождения этого лабораторного задания, вам нужно всего лишь сохранить код и обновить страницу Gmail. Дополнительные развертывания не требуются.
4. Доступ к электронным письмам
Добавьте код, который получает содержимое электронных писем, и разделите код на модули для большей организованности.
Рядом с пунктом «Файлы» нажмите «Добавить».
> Создайте скрипт и файл с именем Cards . Создайте второй скриптовый файл с именем Helpers . Файл Cards.gs создает карточку и использует функции из Helpers.gs для заполнения полей формы на основе содержимого электронного письма.
Замените стандартный код в Cards.gs следующим кодом:
var FIELDNAMES = ['Date', 'Amount', 'Description', 'Spreadsheet URL'];
/**
* Creates the main card users see with form inputs to log expenses.
* Form can be prefilled with values.
*
* @param {String[]} opt_prefills Default values for each input field.
* @param {String} opt_status Optional status displayed at top of card.
* @returns {Card}
*/
function createExpensesCard(opt_prefills, opt_status) {
var card = CardService.newCardBuilder();
card.setHeader(CardService.newCardHeader().setTitle('Log Your Expense'));
if (opt_status) {
if (opt_status.indexOf('Error: ') == 0) {
opt_status = '<font color=\'#FF0000\'>' + opt_status + '</font>';
} else {
opt_status = '<font color=\'#228B22\'>' + opt_status + '</font>';
}
var statusSection = CardService.newCardSection();
statusSection.addWidget(CardService.newTextParagraph()
.setText('<b>' + opt_status + '</b>'));
card.addSection(statusSection);
}
var formSection = createFormSection(CardService.newCardSection(),
FIELDNAMES, opt_prefills);
card.addSection(formSection);
return card;
}
/**
* Creates form section to be displayed on card.
*
* @param {CardSection} section The card section to which form items are added.
* @param {String[]} inputNames Names of titles for each input field.
* @param {String[]} opt_prefills Default values for each input field.
* @returns {CardSection}
*/
function createFormSection(section, inputNames, opt_prefills) {
for (var i = 0; i < inputNames.length; i++) {
var widget = CardService.newTextInput()
.setFieldName(inputNames[i])
.setTitle(inputNames[i]);
if (opt_prefills && opt_prefills[i]) {
widget.setValue(opt_prefills[i]);
}
section.addWidget(widget);
}
return section;
}
Функция createExpensesCard принимает массив значений для предварительного заполнения формы в качестве необязательного аргумента. Функция может отображать необязательное сообщение о состоянии, которое окрашивается в красный цвет, если состояние начинается с "Ошибка:", и в противном случае в зеленый. Вместо добавления каждого поля в форму вручную, вспомогательная функция createFormSection выполняет цикл создания текстовых полей ввода, устанавливает для каждого значение по умолчанию с помощью setValue , а затем добавляет эти поля в соответствующие разделы карточки.
Теперь замените стандартный код в Helpers.gs следующим кодом:
/**
* Finds largest dollar amount from email body.
* Returns null if no dollar amount is found.
*
* @param {Message} message An email message.
* @returns {String}
*/
function getLargestAmount(message) {
return 'TODO';
}
/**
* Determines date the email was received.
*
* @param {Message} message An email message.
* @returns {String}
*/
function getReceivedDate(message) {
return 'TODO';
}
/**
* Determines expense description by joining sender name and message subject.
*
* @param {Message} message An email message.
* @returns {String}
*/
function getExpenseDescription(message) {
return 'TODO';
}
/**
* Determines most recent spreadsheet URL.
* Returns null if no URL was previously submitted.
*
* @returns {String}
*/
function getSheetUrl() {
return 'TODO';
}
Функции в Helpers.gs вызываются функцией getContextualAddon для определения предварительно заполненных значений в форме. На данный момент эти функции будут возвращать только строку "TODO", поскольку логику предварительного заполнения вы реализуете на более позднем этапе.
Далее обновите код в файле GetContextualAddon.gs , чтобы он использовал код из Cards.gs и Helpers.gs . Замените код в GetContextualAddon.gs следующим кодом:
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Returns the contextual add-on data that should be rendered for
* the current e-mail thread. This function satisfies the requirements of
* an 'onTriggerFunction' and is specified in the add-on's manifest.
*
* @param {Object} event Event containing the message ID and other context.
* @returns {Card[]}
*/
function getContextualAddOn(event) {
var message = getCurrentMessage(event);
var prefills = [getReceivedDate(message),
getLargestAmount(message),
getExpenseDescription(message),
getSheetUrl()];
var card = createExpensesCard(prefills);
return [card.build()];
}
/**
* Retrieves the current message given an action event object.
* @param {Event} event Action event object
* @return {Message}
*/
function getCurrentMessage(event) {
var accessToken = event.messageMetadata.accessToken;
var messageId = event.messageMetadata.messageId;
GmailApp.setCurrentMessageAccessToken(accessToken);
return GmailApp.getMessageById(messageId);
}
Обратите внимание на новую функцию getCurrentMessage , которая использует событие, предоставляемое Gmail, для чтения текущего открытого сообщения пользователя. Для работы этой функции добавьте в манифест скрипта дополнительную область видимости, разрешающую доступ к сообщениям Gmail только для чтения.
В appscript.json обновите параметр oauthScopes таким образом, чтобы он также запрашивал область действия https://www.googleapis.com/auth/gmail.addons.current.message.readonly .
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/gmail.addons.current.message.readonly"
],
В Gmail запустите свое дополнение и предоставьте Expense It! доступ к просмотру электронных писем. Теперь поля формы будут автоматически заполнены значением "TODO".
5. Взаимодействие с Google Таблицами
В дополнении Expense It! есть форма, в которую пользователь может ввести подробную информацию о расходах, но эти данные никуда не отправляются. Давайте добавим кнопку, которая будет отправлять данные из формы в таблицу Google Sheets.
Для добавления кнопки мы воспользуемся классом ButtonSet . Для взаимодействия с Google Sheets мы будем использовать сервис Google Sheets .
Измените createFormSection таким образом, чтобы он возвращал кнопку с надписью «Отправить» в разделе формы карточки. Выполните следующие шаги:
- Создайте текстовую кнопку, используя
CardService.newTextButton(), и назовите ее "Отправить", используяCardService.TextButton.setText(). - Настройте кнопку таким образом, чтобы при нажатии на нее вызывалось следующее действие
submitFormс помощьюCardService.TextButton.setOnClickAction():
/**
* Logs form inputs into a spreadsheet given by URL from form.
* Then displays edit card.
*
* @param {Event} e An event object containing form inputs and parameters.
* @returns {Card}
*/
function submitForm(e) {
var res = e['formInput'];
try {
FIELDNAMES.forEach(function(fieldName) {
if (! res[fieldName]) {
throw 'incomplete form';
}
});
var sheet = SpreadsheetApp
.openByUrl((res['Spreadsheet URL']))
.getActiveSheet();
sheet.appendRow(objToArray(res, FIELDNAMES.slice(0, FIELDNAMES.length - 1)));
return createExpensesCard(null, 'Logged expense successfully!').build();
}
catch (err) {
if (err == 'Exception: Invalid argument: url') {
err = 'Invalid URL';
res['Spreadsheet URL'] = null;
}
return createExpensesCard(objToArray(res, FIELDNAMES), 'Error: ' + err).build();
}
}
/**
* Returns an array corresponding to the given object and desired ordering of keys.
*
* @param {Object} obj Object whose values will be returned as an array.
* @param {String[]} keys An array of key names in the desired order.
* @returns {Object[]}
*/
function objToArray(obj, keys) {
return keys.map(function(key) {
return obj[key];
});
}
- Создайте виджет набора кнопок, используя
CardService.newButtonSet(), и добавьте текстовую кнопку в этот набор кнопок с помощьюCardService.ButtonSet.addButton(). - Добавьте виджет с набором кнопок в раздел формы карточки, используя
CardService.CardSection.addWidget().
Всего несколькими строками кода мы можем открыть электронную таблицу по её URL-адресу, а затем добавить в неё строку данных. Обратите внимание, что данные из формы передаются в функцию как часть события e , и мы проверяем, заполнил ли пользователь все поля. Если ошибок не возникает, мы создаём пустую карточку расходов с положительным статусом. В случае обнаружения ошибки мы возвращаем исходную заполненную карточку вместе с сообщением об ошибке. Вспомогательная функция objToArray упрощает преобразование ответов из формы в массив, который затем можно добавить в электронную таблицу.
Наконец, обновите раздел oauthScopes в appsscript.json и снова запросите область действия https://www.googleapis.com/auth/spreadsheets . После авторизации эта область действия позволяет дополнению читать и изменять таблицы Google Sheets пользователя.
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/gmail.addons.current.message.readonly",
"https://www.googleapis.com/auth/spreadsheets"
],
Если вы еще не создали новую электронную таблицу, создайте ее по адресу https://docs.google.com/spreadsheets/ .
Теперь снова запустите надстройку и попробуйте отправить форму. Убедитесь, что вы ввели полный URL-адрес целевого ресурса в поле «URL-адрес электронной таблицы» формы.
6. Сохраняйте значения с помощью службы свойств.
Часто пользователи вносят множество расходов в одну и ту же электронную таблицу, поэтому было бы удобно указывать URL-адрес последней использованной таблицы в качестве значения по умолчанию в карточке. Чтобы знать URL-адрес последней использованной таблицы, нам потребуется сохранять эту информацию каждый раз при использовании дополнения.
Сервис Properties позволяет хранить пары ключ-значение . В нашем случае подходящим ключом будет "SPREADSHEET_URL", а значением — сам URL-адрес. Для хранения такого значения необходимо изменить submitForm в Cards.gs таким образом, чтобы URL-адрес электронной таблицы сохранялся в качестве свойства при добавлении новой строки на лист.
Обратите внимание, что свойства могут иметь одну из трех областей видимости: скрипт, пользователь или документ . Область видимости «документ» не применяется к надстройкам Gmail, хотя она актуальна для отдельного типа надстроек при хранении информации, специфичной для конкретного документа или таблицы Google. Для нашей надстройки желаемое поведение заключается в том, чтобы пользователь видел свою собственную (а не чужую) последнюю электронную таблицу в качестве варианта по умолчанию в форме. Следовательно, мы выбираем область видимости «пользователь» вместо области видимости «скрипт» .
Используйте PropertiesService.getUserProperties().setProperty() для сохранения URL-адреса электронной таблицы. Добавьте следующее в submitForm в Cards.gs :
PropertiesService.getUserProperties().setProperty('SPREADSHEET_URL',
res['Spreadsheet URL']);
Затем измените функцию getSheetUrl в Helpers.gs так, чтобы она возвращала сохраненное свойство, благодаря чему пользователь будет видеть самый последний URL-адрес каждый раз, когда использует надстройку. Используйте PropertiesService.getUserProperties().getProperty() для получения значения свойства.
/**
* Determines most recent spreadsheet URL.
* Returns null if no URL was previously submitted.
*
* @returns {String}
*/
function getSheetUrl() {
return PropertiesService.getUserProperties().getProperty('SPREADSHEET_URL');
}
Наконец, для доступа к сервису Property скрипт также должен быть авторизован. Добавьте в манифест область действия https://www.googleapis.com/auth/script.storage как и раньше, чтобы разрешить вашему дополнению читать и записывать информацию о недвижимости.
7. Проанализируйте сообщение Gmail.
Чтобы действительно сэкономить время пользователей, давайте предварительно заполним форму соответствующей информацией о расходах из электронного письма. Мы уже создали функции в Helpers.gs , которые выполняют эту роль, но пока что они возвращают только "TODO" для даты, суммы и описания расходов.
Например, мы можем получить дату получения электронного письма и использовать ее в качестве значения по умолчанию для даты расходов.
/**
* Determines date the email was received.
*
* @param {Message} message - The message currently open.
* @returns {String}
*/
function getReceivedDate(message) {
return message.getDate().toLocaleDateString();
}
Реализуйте оставшиеся две функции:
-
getExpenseDescriptionможет включать в себя объединение имени отправителя и темы сообщения, хотя существуют и более сложные способы анализа текста сообщения для получения еще более точного описания. - Для
getLargestAmountобратите внимание на конкретные символы, связанные с деньгами. В чеках часто указывается несколько значений, например, налоги и другие сборы. Подумайте, как вы могли бы определить правильную сумму. Регулярные выражения также могут оказаться полезными.
Если вам нужна дополнительная информация, изучите справочную документацию по GmailMessage или посмотрите код решения, который вы скачали в начале практического занятия. После того, как вы разработаете собственные реализации для всех функций в Helpers.gs , протестируйте свой аддон! Откройте чеки и начните записывать их в электронную таблицу!
8. Очистите форму с помощью действий с карточками.
Что произойдет, если Expense It! неправильно определит расход в открытом электронном письме и автоматически заполнит форму неверной информацией? Пользователь очистит форму. Класс CardAction позволяет указать функцию, которая вызывается при нажатии на это действие. Давайте используем его, чтобы предоставить пользователю быстрый способ очистки формы.
Измените createExpensesCard таким образом, чтобы возвращаемая карточка имела действие «Очистить форму», и при нажатии на него вызывалась следующая функция clearForm , которую можно вставить в файл Cards.gs . Вам потребуется передать параметр opt_status с именем «Status» в это действие, чтобы гарантировать, что после очистки формы сообщение о статусе останется. Обратите внимание, что необязательные параметры для действий должны быть типа Object.<string, string>, поэтому, если opt_status недоступен, следует передать {'Status' : ''} .
/**
* Recreates the main card without prefilled data.
*
* @param {Event} e An event object containing form inputs and parameters.
* @returns {Card}
*/
function clearForm(e) {
return createExpensesCard(null, e['parameters']['Status']).build();
}
9. Создайте электронную таблицу.
Помимо редактирования существующих электронных таблиц с помощью Google Apps Script, вы можете программно создать совершенно новую электронную таблицу. Для нашего дополнения давайте позволим пользователю создавать электронную таблицу для учета расходов. Для начала добавьте следующий раздел карточки в карточку, которую возвращает createExpensesCard .
var newSheetSection = CardService.newCardSection();
var sheetName = CardService.newTextInput()
.setFieldName('Sheet Name')
.setTitle('Sheet Name');
var createExpensesSheet = CardService.newAction()
.setFunctionName('createExpensesSheet');
var newSheetButton = CardService.newTextButton()
.setText('New Sheet')
.setOnClickAction(createExpensesSheet);
newSheetSection.addWidget(sheetName);
newSheetSection.addWidget(CardService.newButtonSet().addButton(newSheetButton));
card.addSection(newSheetSection);
Теперь, когда пользователь нажимает кнопку «Новый лист», надстройка генерирует новую электронную таблицу, отформатированную с заголовком, который фиксируется таким образом, что он всегда виден. Пользователь указывает заголовок для новой электронной таблицы в форме, хотя включение значения по умолчанию на случай, если форма пустая, может быть хорошим вариантом. В вашей реализации функции createExpensesSheet верните практически идентичную карточку существующей, добавив соответствующее сообщение о статусе, а также предварительно заполнив поле URL-адресом новой электронной таблицы.
10. Поздравляем!
Вы успешно разработали и внедрили надстройку для Gmail, которая находит расходы в электронном письме и помогает пользователям записывать эти расходы в электронную таблицу всего за несколько секунд. Вы использовали Google Apps Script для взаимодействия с несколькими API Google и сохранения данных между многократными запусками надстройки.
Возможные улучшения
Дайте волю своей фантазии, улучшая Expense It!, но вот несколько идей для создания еще более полезного продукта:
- Ссылка на электронную таблицу появится после того, как пользователь внесет данные о расходах.
- Добавить возможность редактировать/отменять регистрацию расходов.
- Интегрируйте внешние API , чтобы пользователи могли совершать платежи и запрашивать деньги.