程式設計層級:中級
時間長度:45 分鐘
專案類型:Google Workspace 外掛程式
目標
- 瞭解解決方案的功能。
- 瞭解 Apps Script 服務在 解決方案
- 設定環境。
- 設定指令碼。
- 執行指令碼。
認識這項解決方案
顯示與您人員相關的資訊,例如電子郵件、電話號碼和部門 您在使用 Google Workspace 時,可與機構成員協同合作。 當您在回覆 Gmail 郵件時, 編輯 Google 雲端硬碟檔案或查看 Google 日曆活動。
運作方式
指令碼會從運作中的訊息、檔案或事件取得電子郵件地址, 視情境而定,包括 Gmail 郵件收件者、 雲端硬碟檔案編輯者和日曆活動參與者。 這個指令碼只會顯示貴機構電子郵件地址的資訊,
Apps Script 服務
這項解決方案使用下列服務:
- Admin SDK Directory 進階服務:搜尋 Directory API
- 基礎服務:使用工作階段類別 可協助篩選電子郵件地址,但不要在搜尋結果中顯示目前的使用者 也就是預測結果
- 快取服務:先搜尋快取 。
- 日曆服務 - 如果 背景資訊是日曆活動,會從 活動。
- 資訊卡服務:建立使用者介面 外掛程式
- 雲端硬碟服務 - 如果 是雲端硬碟檔案,系統會取得 協作者。
- Gmail 服務:如果背景資訊 Gmail 郵件,取得「收件者」、「副本」和「寄件者」中的電子郵件地址 欄位。
必要條件
- 可存取網際網路的網路瀏覽器。
- Google Workspace 帳戶 (可能需要管理員核准)。
- Google Cloud 專案。
設定環境
在 Google Cloud 控制台中開啟 Cloud 專案
如果沒有開啟,請開啟您要使用的 Cloud 專案 定義:
- 在 Google Cloud 控制台中,前往「選取專案」頁面。
- 選取要使用的 Google Cloud 專案。或是按一下「建立專案」,然後按照畫面上的指示操作。如要建立 Google Cloud 專案,您可能需要開啟該專案的計費功能。
啟用 Admin SDK API
本快速入門導覽課程需要使用 Admin SDK API Directory 進階服務 存取 Admin SDK API。
您必須先在 Google Cloud 專案中啟用這些 Google API,才能使用這些 API。 您可以在單一 Google Cloud 專案中啟用一或多個 API。在 Cloud 專案中開啟 Admin SDK API。
設定 OAuth 同意畫面
必須為 Google Workspace 外掛程式設定同意畫面。設定中 外掛程式的 OAuth 同意畫面,會決定 Google 向使用者顯示的內容。
- 在 Google Cloud 控制台中,前往「選單」圖示 > API 與服務 >「OAuth 同意畫面」。
- 在「使用者類型」部分選取「內部」,然後按一下「建立」。
- 填寫應用程式註冊表單,然後按一下「Save and Continue」。
您現在可以略過新增範圍的步驟,然後按一下「儲存並繼續」。 日後您可以製作並使用應用程式 Google Workspace 機構,您必須將「使用者類型」變更為「外部」, 新增應用程式所需的授權範圍。
- 查看應用程式註冊摘要。如要修改資訊,請按一下「編輯」。如果應用程式 註冊看起來沒有問題,請按一下 [返回資訊主頁]。
設定指令碼
建立 Apps Script 專案
按一下以下按鈕,開啟 Apps Script 團隊清單 專案。
開啟專案按一下「總覽」圖示
。在總覽頁面中,按一下「建立副本」圖示 。
複製 Cloud 專案編號
- 在 Google Cloud 控制台中,前往「選單」圖示 > IAM 與管理員 >「設定」。
- 複製「Project number」欄位中的值。
設定 Apps Script 專案的 Cloud 專案
- 在複製的 Apps Script 專案中, 按一下「Project Settings」圖示 。
- 在「Google Cloud Platform (GCP) Project」(Google Cloud Platform (GCP) 專案) 下方,按一下 [變更專案]。
- 在「GCP 專案編號」中,貼上 Google Cloud 專案編號。
- 按一下「設定專案」。
安裝測試部署作業
- 在複製的 Apps Script 專案中按一下「編輯器」 。
- 開啟
Code.gs
檔案,然後按一下「Run」。當系統提示時,請授權 指令碼 - 依序點選「部署」>「測試部署作業」。
- 依序點選「安裝」>「完成」。
執行指令碼
- 開啟 Gmail 郵件、日曆活動或 雲端硬碟檔案。
- 在右側欄中,開啟「團隊清單」外掛程式 。
- 如果系統顯示提示,請授權外掛程式。
- 這個外掛程式會顯示 或者指出郵件、活動或檔案沒有團隊 成員。
- 如要尋找團隊成員,請按一下「搜尋使用者」,然後輸入姓名或電子郵件地址 讓我們看看 DNS 解析 進一步探索內部和外部位址按一下「搜尋」。
查看程式碼
如要查看這個解決方案的 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 Developers 專家的協助下維護。