Google Workspace でチームメンバーの詳細を確認する

コーディング レベル: 中級
所要時間: 45 分
プロジェクトの種類: Google Workspace アドオン

目標

  • ソリューションの機能を理解する
  • コンテナ内で Apps Script サービスが 説明します。
  • 環境を設定する。
  • スクリプトを設定します。
  • スクリプトを実行します。

このソリューションについて

招待したユーザーに関する情報(メールアドレス、電話番号、部門など)を表示する 組織内で共同作業を行えます。 この情報は、Gmail のメールに返信するときや、 Google ドライブのファイルの編集、Google カレンダーの予定の表示など)。

チームリストの Google Workspace アドオンのスクリーンショット

仕組み

このスクリプトは、アクティブなメッセージ、ファイル、予定からメールアドレスを取得します。 コンテキストによっては、Gmail のメール受信者、 ドライブのファイル エディタ、カレンダーの予定の参加者などです。 このスクリプトは組織内のメールアドレスの情報のみを表示します。

Apps Script サービス

このソリューションでは、次のサービスを使用します。

  • Admin SDK Directory Advanced Service - Directory API
  • 基本サービス - Session クラスを使用します。 メールアドレスをフィルタして、検索結果に現在のユーザーが表示されないようにできます 表示されます。
  • キャッシュ サービス - 最初にキャッシュを検索します。 Directory API で 1 人のユーザーを検索する際に使用できます。
  • カレンダー サービス - コンテキストで、カレンダーの予定からメールアドレスを 選択します。
  • カードサービス - ユーザー インターフェースを作成します。 表示されます。
  • ドライブ サービス - コンテキストが Google ドライブのファイルであり、 ユーザーがアクティブなファイルで共同編集者を表示する権限を持っている場合。
  • Gmail サービス - コンテキストが Gmail のメールで、To、Cc、From からメールアドレスを取得 フィールドがあります。

前提条件

環境の設定

Google Cloud コンソールで Cloud プロジェクトを開く

まだ開いていない場合は、使用する Cloud プロジェクトを開きます。 次のように設定します。

  1. Google Cloud コンソールで、[プロジェクトの選択] ページに移動します。

    Cloud プロジェクトを選択する

  2. 使用する Google Cloud プロジェクトを選択します。または、[プロジェクトを作成] をクリックし、画面の指示に従います。Google Cloud プロジェクトを作成する場合、プロジェクトの課金を有効にする必要があります。

Admin SDK API を有効にする

このクイックスタートでは、Admin SDK API Directory 拡張サービスを使用します。このサービスは、 Admin SDK API にアクセスします。

Google API を使用する前に、Google Cloud プロジェクトで有効にする必要があります。 1 つの Google Cloud プロジェクトで 1 つ以上の API を有効にできます。

Google Workspace アドオンを使用するには、同意画面の構成が必要です。構成 アドオンの OAuth 同意画面は、Google がユーザーに表示する内容を定義します。

  1. Google Cloud コンソールで、メニュー に移動します。 > API とサービス > OAuth 同意画面

    OAuth 同意画面に移動

  2. [ユーザーの種類] で [内部] を選択し、[作成] をクリックします。
  3. アプリ登録フォームに入力し、[保存して次へ] をクリックします。
  4. 現時点では、スコープの追加をスキップして [保存して次へ] をクリックします。 今後、Google Play 以外で使用するアプリを作成した場合、 [ユーザーの種類] を [外部] に変更してから、 アプリに必要な認可スコープを追加します。

  5. アプリ登録の概要を確認します。変更するには、[編集] をクリックします。アプリが 問題がなければ、[ダッシュボードに戻る] をクリックします。

スクリプトを設定する

Apps Script プロジェクトを作成する

  1. 次のボタンをクリックして、チームリスト Apps Script を開きます。 できます。
    プロジェクトを開く

  2. [概要] をクリックします。

  3. 概要ページで「コピーを作成」アイコン コピー作成のアイコン をクリックします。

Cloud プロジェクト番号をコピーする

  1. Google Cloud コンソールで、メニュー に移動します。 > IAM と管理 > [設定] をタップします。

    [IAM と管理] の [設定] に移動

  2. [プロジェクト番号] フィールドに値をコピーします。

Apps Script プロジェクトの Cloud プロジェクトを設定する

  1. コピーした Apps Script プロジェクトに、 [プロジェクトの設定] プロジェクト設定のアイコン をクリックします。
  2. [Google Cloud Platform(GCP)プロジェクト] で、[プロジェクトを変更] をクリックします。
  3. [GCP プロジェクト番号] に、Google Cloud プロジェクト番号を貼り付けます。
  4. [プロジェクトを設定] をクリックします。

テストデプロイをインストールする

  1. コピーした Apps Script プロジェクトで、[Editor] をクリックします。
  2. Code.gs ファイルを開き、[実行] をクリックします。プロンプトが表示されたら、 使用します。
  3. [デプロイ] > [デプロイをテスト] をクリックします。
  4. [インストール] > [完了] をクリックします。

スクリプトを実行する

  1. Gmail のメール、カレンダーの予定、または 。
  2. 右側のサイドバーでチームリスト アドオンを開く
  3. プロンプトが表示されたら、アドオンを承認します。
  4. このアドオンには、 または、メッセージ、予定、ファイルにチームが含まれていないことを示す できます。
  5. チームメンバーを見つけるには、[ユーザーを検索] をクリックし、名前またはメールアドレスを入力します。 あります。[Search] をクリックします。

コードを確認する

このソリューションの Apps Script コードを確認するには、 以下のソースコードを表示します。

ソースコードを表示

コード.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 Developer Experts のサポートのもと、Google によって管理されています。

次のステップ