Nhận thông tin chi tiết về thành viên nhóm trong Google Workspace

Cấp độ lập trình: Trung bình
Thời lượng: 45 phút
Loại dự án: Tiện ích bổ sung của Google Workspace

Mục tiêu

  • Hiểu rõ chức năng của giải pháp.
  • Tìm hiểu chức năng của các dịch vụ Apps Script trong giải pháp.
  • Thiết lập môi trường.
  • Thiết lập tập lệnh.
  • Chạy tập lệnh.

Giới thiệu về giải pháp này

Hiện thông tin, chẳng hạn như email, số điện thoại và phòng ban, về những người mà bạn cộng tác trong tổ chức khi đang làm việc trong Google Workspace. Bạn có thể xem thông tin này khi trả lời thư Gmail, chỉnh sửa tệp trên Google Drive hoặc xem sự kiện trên Lịch Google.

Ảnh chụp màn hình Tiện ích bổ sung Google Workspace cho Danh sách nhóm

Cách hoạt động

Tập lệnh nhận địa chỉ email từ thông báo, tệp hoặc sự kiện đang hoạt động. Tùy thuộc vào ngữ cảnh, đối tượng này có thể bao gồm người nhận thư trong Gmail, trình chỉnh sửa tệp trên Drive và người tham dự sự kiện trên Lịch. Tập lệnh chỉ hiển thị thông tin cho địa chỉ email trong tổ chức của bạn.

Dịch vụ Apps Script

Giải pháp này sử dụng các dịch vụ sau:

  • Dịch vụ nâng cao của Thư mục SDK dành cho quản trị viên – Tìm kiếm những người đang sử dụng API Thư mục.
  • Dịch vụ cơ sở – Sử dụng Lớp phiên để giúp lọc địa chỉ email và không hiển thị người dùng hiện tại trong kết quả tìm kiếm.
  • Dịch vụ bộ nhớ đệm – Tìm kiếm bộ nhớ đệm trước tiên khi tìm một người từ API Thư mục.
  • Dịch vụ Lịch – Nếu ngữ cảnh là một sự kiện trên Lịch, hãy lấy địa chỉ email từ sự kiện đang hoạt động.
  • Dịch vụ thẻ – Tạo giao diện người dùng của tiện ích bổ sung.
  • Dịch vụ Drive: Nếu ngữ cảnh là một tệp trên Drive, hãy lấy địa chỉ email của người cộng tác nếu người dùng có quyền xem những tệp đó trong tệp đang hoạt động.
  • Dịch vụ Gmail: Nếu ngữ cảnh là thư Gmail, thì bạn sẽ nhận được địa chỉ email từ các trường Tới, Cc và Từ trong thư Gmail đang hoạt động.

Điều kiện tiên quyết

Thiết lập môi trường

Mở dự án Cloud trong bảng điều khiển Google Cloud

Nếu nó chưa mở, hãy mở dự án Cloud mà bạn định sử dụng cho mẫu này:

  1. Trong bảng điều khiển của Google Cloud, hãy chuyển đến trang Select a project (Chọn một dự án).

    Chọn một dự án Cloud

  2. Chọn dự án Google Cloud mà bạn muốn sử dụng. Hoặc nhấp vào Tạo dự án rồi làm theo hướng dẫn trên màn hình. Nếu tạo một dự án Google Cloud, bạn có thể cần phải bật tính năng thanh toán cho dự án đó.

Bật API SDK dành cho quản trị viên

Quá trình khởi động nhanh này sử dụng dịch vụ nâng cao của Thư mục API SDK dành cho quản trị viên, giúp truy cập API quản trị SDK. Trước khi sử dụng Google API, bạn cần bật các API đó trong dự án Google Cloud. Bạn có thể bật một hoặc nhiều API trong một dự án Google Cloud.

    Trong dự án Cloud, hãy bật API SDK dành cho quản trị viên.

    Bật API

Tiện ích bổ sung của Google Workspace yêu cầu cấu hình màn hình xin phép. Định cấu hình màn hình đồng ý OAuth của tiện ích bổ sung sẽ xác định nội dung Google hiển thị cho người dùng.

  1. Trong bảng điều khiển của Google Cloud, hãy chuyển đến Trình đơn > API và dịch vụ > Màn hình xin phép bằng OAuth.

    Chuyển đến màn hình xin phép bằng OAuth

  2. Chọn loại người dùng cho ứng dụng của bạn, rồi nhấp vào Tạo.
  3. Hoàn thành biểu mẫu đăng ký ứng dụng, sau đó nhấp vào Lưu và tiếp tục.
  4. Hiện tại, bạn có thể bỏ qua bước thêm phạm vi rồi nhấp vào Lưu và tiếp tục. Trong tương lai, khi tạo một ứng dụng để dùng bên ngoài tổ chức Google Workspace, bạn phải thêm và xác minh phạm vi uỷ quyền mà ứng dụng của bạn yêu cầu.

  5. Nếu bạn chọn Bên ngoài cho loại người dùng, hãy thêm người dùng thử nghiệm:
    1. Trong phần Kiểm thử người dùng, hãy nhấp vào Thêm người dùng.
    2. Nhập địa chỉ email của bạn và những người dùng thử nghiệm được ủy quyền khác, sau đó nhấp vào Lưu và tiếp tục.
  6. Xem lại bản tóm tắt đăng ký ứng dụng của bạn. Để thực hiện thay đổi, hãy nhấp vào Chỉnh sửa. Nếu đơn đăng ký ứng dụng có vẻ ổn, hãy nhấp vào Quay lại trang tổng quan.

Thiết lập tập lệnh

Tạo dự án Apps Script

  1. Nhấp vào nút sau để mở dự án Danh sách nhóm Apps Script.
    Mở dự án

  2. Nhấp vào biểu tượng Tổng quan .

  3. Trên trang tổng quan, hãy nhấp vào Tạo bản sao Biểu tượng tạo bản sao.

Sao chép số dự án trên đám mây

  1. Chuyển đến dự án Cloud của bạn trong bảng điều khiển Google Cloud.
  2. Nhấp vào Cài đặt và Tiện ích > Cài đặt dự án.
  3. Sao chép Số dự án.

Đặt dự án Cloud Script của dự án Apps Script

  1. Trong dự án Apps Script đã sao chép, hãy nhấp vào biểu tượng Project Settings (Cài đặt dự án) Biểu tượng cho chế độ cài đặt dự án.
  2. Trong Dự án Google Cloud Platform (GCP), hãy nhấp vào Thay đổi dự án.
  3. Trong số dự án GCP, hãy dán số dự án trên Google Cloud.
  4. Nhấp vào Đặt dự án.

Cài đặt bản triển khai thử nghiệm

  1. Trong dự án Apps Script đã sao chép, hãy nhấp vào Editor (Trình chỉnh sửa) .
  2. Mở tệp Code.gs rồi nhấp vào Run (Chạy). Khi được nhắc, hãy cho phép tập lệnh.
  3. Nhấp vào Triển khai > Triển khai thử nghiệm.
  4. Nhấp vào Cài đặt > Xong.

Chạy tập lệnh

  1. Mở một thư Gmail, sự kiện trên Lịch hoặc tệp Drive.
  2. Ở thanh bên phải, hãy mở tiện ích bổ sung Danh sách nhóm .
  3. Nếu được nhắc, hãy cho phép tiện ích bổ sung đó.
  4. Tiện ích bổ sung này hiển thị thông tin về các thành viên trong nhóm hoặc cho biết rằng tin nhắn, sự kiện hoặc tệp không có thành viên trong nhóm.
  5. Để tìm các thành viên trong nhóm, hãy nhấp vào Tìm kiếm người và nhập tên hoặc địa chỉ email. Nhấp vào Tìm kiếm.

Xem lại mã

Để xem lại mã Apps Script cho giải pháp này, hãy nhấp vào Xem mã nguồn bên dưới:

Xem mã nguồn

// 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 [];
}

appscript.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"
    }
  }
}

Người đóng góp

Mẫu này được Google duy trì với sự trợ giúp của các Chuyên gia dành cho nhà phát triển của Google.

Các bước tiếp theo