程式設計程度:中級
時長:45 分鐘
專案類型:Google Workspace 外掛程式
目標
- 瞭解解決方案的功能。
- 瞭解 Apps Script 服務在解決方案中的作用。
- 設定環境。
- 設定指令碼。
- 執行指令碼。
認識這項解決方案
在 Google Workspace 中工作時,顯示您在貴機構中協作的人員資訊,例如電子郵件、電話號碼和部門。您可以在回覆 Gmail 郵件、編輯 Google 雲端硬碟檔案或查看 Google 日曆活動時查看這項資訊。
運作方式
指令碼會從有效訊息、檔案或事件中取得電子郵件地址。視情況而定,可能包括 Gmail 郵件收件者、雲端硬碟檔案編輯者和日曆活動出席者。指令碼只會顯示貴機構的電子郵件地址資訊。
Apps Script 服務
本解決方案會使用下列服務:
- Admin SDK Directory 進階服務:使用 Directory API 搜尋使用者。
- 基本服務:使用工作階段類別過濾電子郵件地址,並避免在搜尋結果中顯示目前使用者。
- 快取服務:在透過 Directory API 查詢單一使用者時,會先搜尋快取資料。
- 日曆服務:如果情境是 Google 日曆活動,則會從有效活動取得電子郵件地址。
- 卡片服務:建立外掛程式的使用者介面。
- Google 雲端硬碟服務:如果情境是 Google 雲端硬碟檔案,如果使用者有權在有效檔案中查看,就會取得協作者的電子郵件地址。
- Gmail 服務:如果內容是 Gmail 訊息,則會從有效 Gmail 訊息中的「收件者」、「副本」和「寄件者」欄位取得電子郵件地址。
必要條件
- 可連上網際網路的網路瀏覽器。
- Google Workspace 帳戶 (可能需要管理員核准)。
- Google Cloud 專案。
設定環境
在 Google Cloud 控制台中開啟 Cloud 專案
如果尚未開啟,請開啟要用於本範例的 Cloud 專案:
- 在 Google Cloud 控制台中,前往「Select a project」頁面。
- 選取要使用的 Google Cloud 專案。或者,您也可以按一下「建立專案」,然後按照畫面上的指示操作。如果您建立 Google Cloud 專案,可能需要為專案啟用計費功能。
啟用 Admin SDK API
本快速入門課程會使用 Admin SDK API Directory 進階服務,該服務會存取 Admin SDK API。
使用 Google API 前,您必須先在 Google Cloud 專案中啟用這些 API。您可以在單一 Google Cloud 專案中啟用一或多個 API。在 Cloud 專案中開啟 Admin SDK API。
設定 OAuth 同意畫面
Google Workspace 外掛程式需要設定同意畫面。設定外掛程式的 OAuth 同意畫面,可定義 Google 向使用者顯示的內容。
- 在 Google Cloud 控制台中,依序前往「選單」>「API 和服務」 >「OAuth 同意畫面」。
- 在「使用者類型」部分,選取「內部」,然後按一下「建立」。
- 填寫應用程式註冊表單,然後按一下「儲存並繼續」。
目前您可以略過新增範圍,直接按一下「儲存並繼續」。日後,如果您建立的應用程式是用於 Google Workspace 機構以外的環境,就必須將使用者類型變更為外部,然後新增應用程式所需的授權範圍。
- 查看應用程式註冊摘要。如要修改資訊,請按一下「編輯」。如果應用程式註冊看起來沒問題,請按一下「Back to Dashboard」。
設定指令碼
建立 Apps Script 專案
點選下方按鈕,開啟「團隊名單」應用程式腳本專案。
開啟專案按一下「總覽」圖示
。在總覽頁面中,按一下「建立副本」圖示 。
複製 Cloud 專案編號
- 在 Google Cloud 控制台中,依序前往「Menu」(選單) >「IAM & Admin」(IAM 與管理)>「Settings」(設定)。
- 在「專案編號」欄位中複製值。
設定 Apps Script 專案的 Cloud 專案
- 在複製的 Apps Script 專案中,按一下「Project Settings」圖示 。
- 在「Google Cloud Platform (GCP) 專案」下方,按一下「變更專案」。
- 在「GCP 專案編號」中貼上 Google Cloud 專案編號。
- 按一下「設定專案」。
安裝測試部署作業
- 在複製的 Apps Script 專案中,按一下「編輯器」圖示 。
- 開啟
Code.gs
檔案,然後按一下「Run」。出現提示時,請授權執行指令碼。 - 依序按一下「部署」>「測試部署作業」。
- 依序點選「安裝」>「完成」。
執行指令碼
- 開啟 Gmail 訊息、日曆活動或 Google 雲端硬碟檔案。
- 在右側邊欄中,開啟「團隊名單」外掛程式 。
- 如果出現提示,請授權使用外掛程式。
- 外掛程式會顯示團隊成員的相關資訊,或指出訊息、活動或檔案沒有團隊成員。
- 如要尋找團隊成員,請按一下「搜尋使用者」,然後輸入名稱或電子郵件地址。按一下「搜尋」。
查看程式碼
如要查看這個解決方案的 Apps Script 程式碼,請按一下下方的「查看原始碼」:
查看原始碼
Code.gs
// Copyright 2022 Google Inc. All Rights Reserved. // // 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 // // http://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. // Sample Google Workspace Add-on that displays profile information about people // the user is collaborating with. Collaborators are based on the context -- // recipients of a gmail message, Drive file ACLs, or event attendees. // // Profile information is from the Directory API in the Admin SDK. As a result, // the add-on only shows information for email addresses in the same domain // as as the current user. Different strategies can be used for other use cases, // such as integration with a CRM where the focus may be on external email // addresses/customers. // See https://github.com/contributorpw/lodashgs var _ = LodashGS.load(); /** * Renders the home page for the add-on. Used in all host apps when * no context selected. * * @param {Object} event - current add-on event * @return {Card[]} Card(s) to display */ function onHomePage(event) { var card = buildSearchCard_(); return [card]; } /** * Renders the contextual interface for a Gmail message. * * @param {Object} event - current add-on event * @return {Card[]} Card(s) to display */ function onGmailMessageSelected(event) { var emails = extractEmailsFromMessage_(event); var people = fetchPeople_(emails); if (people.length == 0) { var card = buildSearchCard_("No team members found for current message."); return [card]; } var card = buildTeamListCard_(people) return [card]; } /** * Renders the contextual interface for a calendar event. * * @param {Object} event - current add-on event * @return {Card[]} Card(s) to display */ function onCalendarEventOpen(event) { var emails = extractEmailsFromCalendarEvent_(event); var people = fetchPeople_(emails); if (people.length == 0) { var card = buildSearchCard_("No team members found for current event."); return [card]; } var card = buildTeamListCard_(people) return [card]; } /** * Renders the contextual interface for a selected Drive file. * * @param {Object} event - current add-on event * @return {Card[]} Card(s) to display */ function onDriveItemsSelected(event) { // For demo, only allow single select on files. if (event.drive.selectedItems.length != 1) { var message = "To view team members collaborating on a file, select one file only."; var card = buildSearchCard_(message); return [card]; } var selectedItem = event.drive.selectedItems[0]; if (!selectedItem.addonHasFileScopePermission) { // Need file access to read ACL, ask user to authorize. var authorizeFilesAction = CardService.newAction() .setFunctionName("onAuthorizeDriveFiles") .setLoadIndicator(CardService.LoadIndicator.SPINNER) .setParameters({id: selectedItem.id}); var authorizationMessage = CardService.newTextParagraph() .setText("To view the people on your team the file is shared with, click *Authorize* to grant access."); var authorizeButton = CardService.newTextButton() .setText("Authorize") .setOnClickAction(authorizeFilesAction); var card = CardService.newCardBuilder() .addSection(CardService.newCardSection() .addWidget(authorizationMessage) .addWidget(authorizeButton)) .build(); return [card]; } // Have access, extract ACLs to find co-workers var emails = extractEmailsFromDrivePermissions_(event); var people = fetchPeople_(emails); if (people.length == 0) { var card = buildSearchCard_("No team members found for current file."); return [card]; } var card = buildTeamListCard_(people) return [card]; } /** * Handles the click for requesting drive file access. * * @param {Object} event - current add-on event * @return {ActionResponse} Request to authorize access to a drive item */ function onAuthorizeDriveFiles(event) { var id = event.parameters.id; return CardService.newDriveItemsSelectedActionResponseBuilder() .requestFileScope(id) .build(); } /** * Handles the user search request. * * @param {Object} event - current add-on event * @return {Card[]} Card(s) to display */ function onSearch(event) { if (!event.formInputs || !event.formInputs.query) { var notification = CardService.newNotification() .setText("Enter a query before searching."); return CardService.newActionResponseBuilder() .setNotification(notification) .build(); } var query = event.formInputs.query[0]; var people = queryPeople_(query); if (!people || people.length == 0) { var notification = CardService.newNotification().setText("No people found."); return CardService.newActionResponseBuilder() .setNotification(notification) .build(); } var card = buildTeamListCard_(people); var navigation = CardService.newNavigation().pushCard(card); return CardService.newActionResponseBuilder() .setNavigation(navigation) .build(); } /** * Handles the drill down to view detailed information about a person. * * @param {Object} event - current add-on event * @return {Card[]} Card(s) to display */ function onShowPersonDetails(event) { var person = fetchPerson_(event.parameters.email); var card = buildPersonDetailsCard_(person); return [card] } /** * Builds a card for displaying detailed information about a team member. Currently only shows * a small subset of available information for demo purposes. * * @param {Object} person - User object from the Directory API * @return {Card} Card to display */ function buildPersonDetailsCard_(person) { var photoUrl = person.thumbnailPhotoUrl ? person.thumbnailPhotoUrl : "https://ssl.gstatic.com/s2/profiles/images/silhouette200.png"; var cardHeader = CardService.newCardHeader() .setImageUrl(photoUrl) .setImageStyle(CardService.ImageStyle.CIRCLE) .setTitle(person.name.fullName) if (person.organizations && person.organizations.length) { cardHeader.setSubtitle(person.organizations[0].title); } var section = CardService.newCardSection(); if (person.emails) { person.emails.forEach(function(email) { section.addWidget(CardService.newKeyValue() .setIcon(CardService.Icon.EMAIL) .setContent(email.address)); }); } if (person.phones) { person.phones.forEach(function(phone) { section.addWidget(CardService.newKeyValue() .setIcon(CardService.Icon.PHONE) .setContent(phone.value)); }); } if (person.organizations) { person.organizations.forEach(function(org) { section.addWidget(CardService.newKeyValue() .setIcon(CardService.Icon.MEMBERSHIP) .setContent(org.department)); }); } if (person.locations) { person.locations.forEach(function(location) { var formattedLocation = Utilities.formatString("%s<br>%s", location.area, location.buildingId); section.addWidget(CardService.newKeyValue() .setIcon(CardService.Icon.MAP_PIN) .setContent(formattedLocation)); }); } return CardService.newCardBuilder() .setHeader(cardHeader) .addSection(section) .build(); } /** * Builds a card for displaying a list of people * * @param {Object[]} people - Array of users from the Directory API * @return {Card} Card to display */ function buildTeamListCard_(people) { var resultsSection = CardService.newCardSection(); people.forEach(function(person) { var photoUrl = person.thumbnailPhotoUrl ? person.thumbnailPhotoUrl : "https://ssl.gstatic.com/s2/profiles/images/silhouette200.png"; var title = person.organizations ? person.organizations[0].title : null; var clickAction = CardService.newAction() .setFunctionName("onShowPersonDetails") .setLoadIndicator(CardService.LoadIndicator.SPINNER) .setParameters({email: person.primaryEmail}); var personSummaryWidget = CardService.newKeyValue() .setContent(person.name.fullName) .setIconUrl(photoUrl) .setOnClickAction(clickAction); if (person.organizations && person.organizations.length) { personSummaryWidget.setBottomLabel(person.organizations[0].title); } resultsSection.addWidget(personSummaryWidget); }); return CardService.newCardBuilder() .addSection(resultsSection) .build(); } /** * Builds the search interface for looking up people. * * @param {string} opt_error - Optional message to include (typically when * contextual search failed.) * @return {Card} Card to display */ function buildSearchCard_(opt_error) { var banner = CardService.newImage() .setImageUrl('https://storage.googleapis.com/gweb-cloudblog-publish/original_images/Workforce_segmentation_1.png'); var searchField = CardService.newTextInput() .setFieldName("query") .setHint("Name or email address") .setTitle("Search for people"); var onSubmitAction = CardService.newAction() .setFunctionName("onSearch") .setLoadIndicator(CardService.LoadIndicator.SPINNER); var submitButton = CardService.newTextButton() .setText("Search") .setOnClickAction(onSubmitAction); var section = CardService.newCardSection() .addWidget(banner) .addWidget(searchField) .addWidget(submitButton); if (opt_error) { var message = CardService.newTextParagraph() .setText("Note: " + opt_error); section.addWidget(message); } return CardService.newCardBuilder() .addSection(section) .build(); } /** * Extracts email addresses from the selected Gmail message. Grabs all emails * from the to/cc/from headers. * * @param {Object} event - current add-on event * @return {string[]} Array of email addresses. */ function extractEmailsFromMessage_(event) { // Fetch currently selected message var accessToken = event.messageMetadata.accessToken; var messageId = event.messageMetadata.messageId; GmailApp.setCurrentMessageAccessToken(accessToken); var message = GmailApp.getMessageById(messageId); if (!message) { return []; } // Parse/emit any email addresses in the to/cc/from headers var splitEmailsRegexp = /\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}\b/gi; var emails = _.union( message.getTo().match(splitEmailsRegexp), message.getCc().match(splitEmailsRegexp), message.getFrom().match(splitEmailsRegexp) ); // Remove any +suffixes in the user name portion to get the canonical email var normalizeRegexp = /(.*)\+.*@(.*)/; emails = emails.map(function(email) { return email.replace(normalizeRegexp, "$1@$2"); }); return filterAndSortEmails_(emails); } /** * Extracts email addresses from the selected Drive item. Grabs all emails * from the file ACLs (if user has permission to view them.) * * @param {Object} event - current add-on event * @return {string[]} Array of email addresses. */ function extractEmailsFromDrivePermissions_(event) { // Make sure just 1 file selected. if (event.drive.selectedItems.length != 1) { return []; } var itemId = event.drive.selectedItems[0].id; var emails = []; var item = Drive.Files.get(itemId, {fields: "owners, sharingUser"}); if (item.sharingUser) { emails.push(item.sharingUser.emailAddress); } if (item.owners) { item.owners.forEach(function(owner) { emails.push(owner.emailAddress); }); } try { var permissions = Drive.Permissions.list(itemId, {fields: '*'}); if (permissions) { permissions.permissions.forEach(function(permission) { if (permission.type != 'domain') { emails.push(permission.emailAddress); } }); } } catch (e) { // Ignore inability to fetch permissions, may not have access console.warn(e); } return filterAndSortEmails_(emails) } /** * Extracts email addresses from the selected calendar event (attendees.) * * @param {Object} event - current add-on event * @return {string[]} Array of email addresses. */ function extractEmailsFromCalendarEvent_(event) { if (!event.calendar || !event.calendar.attendees) { return []; } var emails = event.calendar.attendees.map(function(attendee) { return attendee.email; }); return filterAndSortEmails_(emails); } /** * Filter email addresses to include only those in the same * domain and excluding the current user. * * @param {string[]} emails - Array of email addresses * @return {string[]} */ function filterAndSortEmails_(emails) { if (!emails) { return []; } var userEmail = Session.getActiveUser().getEmail(); var domain = userEmail.slice(userEmail.indexOf('@') + 1); emails = emails.filter(function(email) { return _.endsWith(email, domain) && email != userEmail; }); emails = _.uniq(emails); return emails.sort(); } /** * Look up one or more people from the Directory API. May omit items * if email addresses aren't valid domain users. * * @param {string[]} emails - Array of email addresses to fetch * @return {Object[]} Array of user objects. */ function fetchPeople_(emails) { if (!emails || emails.length == 0) { return []; } return emails.map(fetchPerson_).filter(function(item) { return item != null && item.primaryEmail; }); } /** * Look up a single person from the Directory API. * * @param {string} email - Email addresses to fetch * @return {Object} User object or null if not a valid user */ function fetchPerson_(email) { if (!email) { return null; } // Check cache first var person = CacheService.getUserCache().get(email); if (person && person.primaryEmail) { return JSON.parse(person); } try { person = AdminDirectory.Users.get( email, { projection: 'full', viewType: 'domain_public'}); CacheService.getUserCache().put(email, JSON.stringify(person)); return person; } catch (e) { // Ignore error, may not be valid domain user anymore. console.warn(e); } return null; } /** * Search for people from the Directory API by name or email address. * * @param {string} query - Name or email address to search for. * @return {Object[]} Array of user objects. */ function queryPeople_(query) { try { var options = { query: query, maxResults: 10, customer: 'my_customer', projection: 'full', viewType: 'domain_public' }; var results = AdminDirectory.Users.list(options); var cacheValues = results.users.reduce(function(map, person) { map[person.primaryEmail] = JSON.stringify(person); return map; }, {}); CacheService.getUserCache().putAll(cacheValues); return results.users; } catch (e) { // Ignore error console.warn(e); } return []; }
appsscript.json
{ "timeZone": "America/Denver", "dependencies": { "enabledAdvancedServices": [{ "userSymbol": "Drive", "serviceId": "drive", "version": "v3" }, { "userSymbol": "AdminDirectory", "serviceId": "admin", "version": "directory_v1" }], "libraries": [{ "userSymbol": "LodashGS", "libraryId": "1SQ0PlSMwndIuOAgtVJdjxsuXueECtY9OGejVDS37ckSVbMll73EXf2PW", "version": "5" }] }, "exceptionLogging": "STACKDRIVER", "oauthScopes": [ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/admin.directory.user.readonly", "https://www.googleapis.com/auth/gmail.addons.execute", "https://www.googleapis.com/auth/gmail.addons.current.message.metadata", "https://www.googleapis.com/auth/calendar.addons.execute", "https://www.googleapis.com/auth/calendar.addons.current.event.read", "https://www.googleapis.com/auth/drive.addons.metadata.readonly", "https://www.googleapis.com/auth/drive.file" ], "urlFetchWhitelist": [], "runtimeVersion": "V8", "addOns": { "common": { "name": "Team List", "logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/people_black_24dp.png", "layoutProperties": { "primaryColor": "#4285f4", "secondaryColor": "#ea4335" }, "homepageTrigger": { "runFunction": "onHomePage", "enabled": true }, "universalActions": [{ "label": "Feedback", "openLink": "https://github.com/googleworkspace/add-ons-samples/issues" }], "openLinkUrlPrefixes": [ "https://github.com/googleworkspace/add-ons-samples/" ] }, "gmail": { "contextualTriggers": [{ "unconditional": { }, "onTriggerFunction": "onGmailMessageSelected" }] }, "drive": { "homepageTrigger": { "runFunction": "onHomePage", "enabled": true }, "onItemsSelectedTrigger": { "runFunction": "onDriveItemsSelected" } }, "calendar": { "homepageTrigger": { "runFunction": "onHomePage", "enabled": true }, "eventOpenTrigger": { "runFunction": "onCalendarEventOpen" }, "currentEventAccess": "READ" } } }
貢獻者
這個範例是由 Google 維護,並由 Google 開發人員專家提供協助。