1. Обзор
В этой лаборатории кода вы будете использовать скрипт Google Apps для написания надстройки Google Workspace для Gmail, которая позволит пользователям добавлять данные о квитанциях из электронного письма в электронную таблицу непосредственно в Gmail. Когда пользователь получает квитанцию по электронной почте, он открывает надстройку, которая автоматически получает соответствующую информацию о расходах из электронной почты. Пользователь может редактировать информацию о расходах, а затем отправлять ее для регистрации своих расходов в электронной таблице Google Sheets.
Что вы узнаете
- Создайте надстройку Google Workspace для Gmail с помощью скрипта Google Apps.
- Анализ электронной почты с помощью скрипта Google Apps
- Взаимодействие с Google Sheets через скрипт Google Apps
- Сохраняйте пользовательские значения с помощью службы свойств 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 назовет ваш файл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 Таблицами
Расход это! В дополнении есть форма, позволяющая пользователю ввести информацию о расходах, но эти данные некуда девать. Давайте добавим кнопку, которая отправляет данные формы в Google Sheet.
Чтобы добавить кнопку, мы воспользуемся классом 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 Таблицы пользователя.
"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-адреса в поле формы URL-адреса электронной таблицы .
6. Сохраните значения с помощью службы свойств.
Часто пользователи регистрируют множество расходов в одной и той же электронной таблице, поэтому было бы удобно предложить самый последний URL-адрес таблицы в качестве значения по умолчанию на карточке. Чтобы узнать URL-адрес самой последней электронной таблицы, нам необходимо сохранять эту информацию каждый раз, когда используется надстройка.
Служба свойств позволяет нам хранить пары ключ-значение . В нашем случае разумным ключом будет «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-адресом URL-адреса новой электронной таблицы.
10. Поздравляем!
Вы успешно разработали и внедрили надстройку Gmail, которая находит расходы в электронном письме и помогает пользователям регистрировать расходы в электронной таблице всего за несколько секунд. Вы использовали скрипт Google Apps для взаимодействия с несколькими API Google и сохраняли данные между несколькими запусками надстройки.
Возможные улучшения
Позвольте своему воображению вести вас при усовершенствовании Expense It!, но вот несколько идей, как сделать еще более полезный продукт:
- Ссылка на электронную таблицу после того, как пользователь зарегистрировал расходы
- Добавить возможность редактировать/отменять регистрацию расходов.
- Интегрируйте внешние API , чтобы пользователи могли совершать платежи и запрашивать деньги.