SalesforceからGoogle Chatへメッセージを送ってみた

# 概要
SalesforceからGoogle Chatへメッセージを送信する方法を紹介します。今回は、Lightning Web Component (LWC) と Apex を使用して、簡単なメッセージ送信の手順を説明します。

![CleanShot 2025-03-10 at 02.42.48.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/c483c696-d846-4f1a-ae61-af9989fe8dd2.gif)

![CleanShot 2025-03-10 at 02.01.30@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/5c7276be-c26e-465b-bc8d-94e01f5015d7.png)

# 準備
[Google Chat APIの設定](https://qiita.com/masahiro_sf/items/a0144f91f5d165ad939f#google-chat-api%E3%81%AE%E8%A8%AD%E5%AE%9A)と[Salesforceの設定](https://qiita.com/masahiro_sf/items/a0144f91f5d165ad939f#salesforce%E3%81%AE%E8%A8%AD%E5%AE%9A)を行います。
Google Chat APIは、console.cloud.google.comから、SalesforceはSetupsから行います。

## Google Chat APIの設定
まず、Google Chat APIを有効にし、必要な認証情報を取得します。Google Cloud Consoleでプロジェクトを作成し、APIとサービスの認証情報からOAuth 2.0クライアントIDを作成します。

### Google Chat APIの有効化
`console.cloud.google.com` よりログインし、APIs & Servicesより「Google Chat API」を検索し有効化します。
![CleanShot 2025-03-10 at 01.03.15@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/7369f276-0aa2-46e2-b182-5344ebd8941d.png)

### 有効化
![CleanShot 2025-03-10 at 01.02.50@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/dfee6b6d-c9ed-406c-b114-94dce1623ea5.png)


### Projectを作成
console.google.comにログインし、上部３点ドットの箇所をクリックして新規プロジェクトを作成します。
![CleanShot 2025-03-10 at 00.34.00@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/b8127d20-9024-41e1-b95b-e9d4b634a667.png)

### 新規Credentialの作成
その次に、認証情報でNew Credentialで新しい認証情報を作成します。
![CleanShot 2025-03-10 at 00.35.58@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/c6c10d72-7ef1-4218-8ced-24e68cd187b5.png)

### Callback URLの入力
作成例ですが、名前は任意で「Salesforce」とします。また承認済みのリダイレクトURIですが、こちらは、後ほどSalesforce側でAuth. Providerを作成したのち、Callback URLとして表示されるものをコピペします。複数入力可能なので、ProductionとSandboxでそれぞれ違うAuth. ProviderにあるCallback URLを入れておくとどちらのAuth. Providerも対応できます。

![CleanShot 2025-03-10 at 00.29.51@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/bfa71efc-62db-4d11-84a4-d32614c116d5.png)

### Client ID/ Client Secretの取得
また、上記認証画面のAdditional Informationにある`Client ID`と`Client Secret`は、後ほどSalesforce側のAuth. Provider設定で利用します。

## Salesforceの設定
Auth. Provider, External Credentials, Named Credentials, Permission Setを設定します。

### Auth. Providerの設定
Auth. Providerでは以下のような設定です。上記のClient IDとClient Secretをそれぞれ`Consumer Key`と`Consumer Secret`へコピペします。

| Field | Value |
|:-|:-|
|  Authorize Endpoint URL |  `https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force` |
| Token Endpoint URL  | `https://oauth2.googleapis.com/token`  |
| Default Scopes | ```openid email profile https://www.googleapis.com/auth/chat https://www.googleapis.com/auth/chat.spaces https://www.googleapis.com/auth/chat.messages``` |

![CleanShot 2025-03-10 at 00.45.16@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/20b55156-27da-4ba4-9e97-176674b6951d.png)

また、保存すると下の方にCallback URLが生成されるのでそちらをコピペして、先ほどの[Callback URLの入力](https://qiita.com/masahiro_sf/items/a0144f91f5d165ad939f#Salesforceの設定)へコピペして入力しておきます。
![CleanShot 2025-03-10 at 00.57.43@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/96089344-7533-4e32-a6d9-b80e928a70a4.png)

### External Credentialsの作成
設定でNamed Credentialsで検索した画面で、`External Credentials` タブをクリック、新規`New`ボタンを押します。
![CleanShot 2025-03-10 at 01.09.17@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/6a4e5233-2461-4016-bdcf-1c8eff27a0d1.png)

こちらで新たなExternal Credentialsを作成します。名前は任意ですが、今回は`GoogleChatApi`と名付けます。Authentication Protpcolは`OAuth 2.0`、Authentication Flow Typeは`Browser Flow` Identity Providerは先ほど作成したAuth. Providerの`Google Chat`を選択します。
![CleanShot 2025-03-09 at 22.50.00@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/e0df0d41-c7be-4abd-9c8d-bf16f6c98310.png)

### Named Credentialsを作成
次にExternal Credentialを利用したNamed Credentialsを作成します。
Named Credentialsの画面に戻り、今度はNamed Credentialsのタブを押し、そこの新規を押します。
![CleanShot 2025-03-10 at 01.15.00@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/8d2ba204-8e75-4a50-b475-7316dbc71681.png)

するとNamed Credentialsの設定画面になるので、そちらを入力します。Labelは、任意ですが`GoogleChatApi`、URLは`https://chat.googleapis.com`、Enabled for Calloutsは`有効` External Credentialは先ほど作成した`GoogleChatApi`を選択します。
![CleanShot 2025-03-09 at 22.52.03@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/97ff78ca-aa92-4c5f-9b0d-fc6a61c2a55f.png)

### Principalの設定

こちらPrincipalですが、parameter名は任意ですが`Spaces`と名付けます。Sequence Numberは`1`, Identity Typeは`Per User Principal`、Scopeは`https://googleapis.com/auth/chat` です。
Per User Principalにすることで、次のApexでfindDirectMessageがユーザーにおける相手のIDまたはemailアドレスだけでチャットのスペースが絞れることになります。
![CleanShot 2025-03-09 at 22.54.50@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/1e05f02f-17dc-4d86-94ab-96ad38bb6a5b.png)

Per User Principalとすることで、各ユーザーがそれぞれ認証する必要があります。それぞれのユーザーの設定画面から、External Credentialを選び認証を行ってください。 
![CleanShot 2025-03-09 at 22.56.05@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/1b06c9b4-d04b-42e0-a409-6934b891cab9.png)

Permission Setを割り当てないとここのAllow Accessはエラーになります。後述する[Permission Setの作成](https://qiita.com/masahiro_sf/items/a0144f91f5d165ad939f#permission-set%E3%81%AE%E4%BD%9C%E6%88%90)で割り当てをユーザーにしてからAllow Accessに再度トライしてみてください。
![CleanShot 2025-03-09 at 22.56.56@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/36b1848f-d1e0-4bce-aa33-99b969cbebad.png)


### Apexクラスの作成
次に、Google Chatにメッセージを送信するためのApexクラスを作成します。Methodは２つで `findExistingGoogleChatSpace`（spaceIdを取得する）、`sendGoogleChatMessage` (directMessageを送る)です。
<details><summary>GoogleChatApiController</summary>

```apex:GoogleChatApiController
public with sharing class GoogleChatApiController {
    private static final String GOOGLE_CHAT_API_URL = 'callout:GoogleChatApi/v1';
    
    @AuraEnabled
    public static Map<String, Object> findExistingGoogleChatSpace(String chatSpaceName) {
        try {
            HttpRequest req = new HttpRequest();
            String endpoint = GOOGLE_CHAT_API_URL + '/spaces:findDirectMessage?name=users/' + EncodingUtil.urlEncode(chatSpaceName, 'UTF-8');
            req.setEndpoint(endpoint);
            req.setMethod('GET');
            req.setHeader('Content-Type', 'application/json');
            
            Http http = new Http();
            HttpResponse res = http.send(req);

            if (res.getStatusCode() == 200) {
                Map<String, Object> responseBody = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
                return responseBody;
            } else if (res.getStatusCode() == 404) {
                System.debug('No existing DM space found with the user: ' + chatSpaceName);
            } else {
                System.debug('Failed to retrieve DM space. Response: ' + res.getBody());
            }
        } catch (Exception e) {
            System.debug('Exception occurred while finding existing Google Chat space: ' + e.getMessage());
        }
        return null;
    }

    @AuraEnabled
    public static Map<String, Object> sendGoogleChatMessage(String spaceId, String message) {
        if (String.isEmpty(spaceId) || String.isEmpty(message)) {
            throw new IllegalArgumentException('Space ID and message are required.');
        }
        HttpRequest req = new HttpRequest();
        req.setEndpoint(GOOGLE_CHAT_API_URL + '/' + spaceId + '/messages'); // spaceId is in format as 'spaces/xxxxx'
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');

        Map<String, Object> requestBody = new Map<String, Object>{
            'text' => message
        };        
        req.setBody(JSON.serialize(requestBody));

        Http http = new Http();
        HttpResponse res = http.send(req);
        
        Map<String, Object> resopnseBody = new Map<String, Object>();
        if (res.getStatusCode() != 200 && res.getStatusCode() != 201) {
            throw new CalloutException('Failed to send Google Chat message: ' + res.getBody());
        } else {
            resopnseBody = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            return resopnseBody;
        }
    }
}
```
</details>



### Lightning Web Component (LWC) の作成
次に、LWCを作成し、ユーザーインターフェースとロジックを構築します。
`@wire`でuserのEmailを自動取得したり、[lightning-record-picker](https://developer.salesforce.com/docs/component-library/bundle/lightning-record-picker)を活用すると実装が簡単です。

<details><summary>googleChat.html</summary>

```html:googleChat.html
<template>
    <lightning-card">
        <div class="slds-grid slds-grid_vertical-align-center slds-m-around_x-small">
            <img src={googleChatIcon} width="32" alt="Google Chat" class="slds-m-right_small" />
            <h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate">Google Chat</h2>
        </div>
        <div class="slds-m-around_medium">
            <div class="slds-m-bottom_small" style="position:relative;">
                <lightning-record-picker required object-api-name="User" icon-name="standard:user" size="small"
                    label="Send to" placeholder="Search..." filter={filter} value={selectedUserId}
                    display-info={displayInfo} onchange={handleUserChange}></lightning-record-picker>
                <lightning-textarea disabled={messageDisabled} label="Message" value={message}
                    onchange={handleMessageChange}></lightning-textarea>
                <lightning-spinner if:true={isLoading} alternative-text="Loading" size="x-small"></lightning-spinner>
            </div>
            <lightning-button label="Send Message" onclick={handleSendMessage}></lightning-button>
        </div>
    </lightning-card>
</template>
```
</details>


<details><summary>googleChat.js</summary>

```javascript:googleChat.js
import { LightningElement, wire, api } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import findExistingGoogleChatSpace from '@salesforce/apex/GoogleChatApiController.findExistingGoogleChatSpace';
import sendGoogleChatMessage from '@salesforce/apex/GoogleChatApiController.sendGoogleChatMessage';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import googleChatIcon from '@salesforce/resourceUrl/GoogleChatIcon';

export default class GoogleChatApi extends LightningElement {
    @api recordId;
    chatSpaceName;
    selectedUserId;
    spaceId;
    message = '';
    messageDisabled;
    isLoading = false;
    googleChatIcon = googleChatIcon;

    @wire(getRecord, { recordId: '$selectedUserId', fields: ['User.Name', 'User.Email'] })
    user({ error, data }) {
        if (error) {
            console.error(error);
        } else if (data) {
            const chatSpaceName = data.fields.Email.value;
            this.getSpaceId(chatSpaceName);
        }
    }

    async getSpaceId(chatSpaceName) {
        const space = await findExistingGoogleChatSpace({ chatSpaceName: chatSpaceName });
        this.spaceId = space.name;
    }

    get filter() {
        return {
            criteria: [
                {
                    fieldPath: 'IsActive',
                    operator: 'eq',
                    value: true
                },
                {
                    fieldPath: 'UserType',
                    operator: 'eq',
                    value: 'Standard'
                },
            ]
        };
    }

    handleUserChange(event) {
        const selectedUserId = event.detail.recordId;
        this.messageDisabled = false;
        this.selectedUserId = selectedUserId;
    }

    handleMessageChange(event) {
        this.message = event.target.value;
    }

    handleSendMessage() {
        if (!this.spaceId || !this.message) {
            this.showToast('Error', 'Space ID and message are required', 'error');
            return;
        }

        this.isLoading = true;
        const baseUrl = window.location.origin;
        const finalMessage = this.message + '\n' + '<' + baseUrl + '/' + this.recordId + '|View in Salesforce>';
        sendGoogleChatMessage({ spaceId: this.spaceId, message: finalMessage })
            .then(() => {
                this.isLoading = false;
                this.showToast('Success', 'Message sent successfully', 'success');
                this.finishSendMessage();
            })
            .catch(error => {
                this.isLoading = false;
                this.showToast('Error', 'Failed to send message', 'error');
                console.error(error);
            });
    }

    showToast(title, message, variant) {
        const event = new ShowToastEvent({
            title,
            message,
            variant
        });
        this.dispatchEvent(event);
    }

    finishSendMessage() {
        this.message = '';
        this.messageDisabled = true;
        this.selectedUserId = null;
        this.spaceId = null;
    }
}
```
</details>

<details><summary>googleChatApi.js-meta.xml</summary>

```xml:googleChatApi.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>63.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>
```
</details>


### Permission Setの作成

最後に、Permisssion Setを作成します。上記で作成した設定を利用できるPermissionをまとめて、ユーザーに割り当てていくことで利用可能になります。Permission名は`GoogleChatApi`とします。
External Credential Principal Access (GoogleChatApi - Spaces)
![CleanShot 2025-03-09 at 22.58.29@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/de8a9cc6-43ac-4a0a-b33d-437f2a2c6ff9.png)

Apex Class Access (GoogleChatApiController)
![CleanShot 2025-03-09 at 23.06.14@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/dd389f59-5a4b-4ab4-babf-b9dd2f227c3a.png)

割り当ては以下のボタンからユーザーを選択して割り当てます。
![CleanShot 2025-03-10 at 01.43.51@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/3489b83d-db05-427f-805c-1ce32d084e66.png)

### 個人認証
![CleanShot 2025-03-09 at 22.56.05@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/1053dd35-fbaa-432f-b458-17b61d888ae4.png)

① 右上アイコン  
②（個人の）Settings 
③（個人の）External Credentials  
④ Allow Access
で認証が始まります。利用するユーザーすべて個人で認証するよう依頼してください。Allow Accessを押すとGoogleの認証画面に飛びます。許可するものをExternal CredentialのScopeで設定したものが反映されます。
![CleanShot 2025-03-09 at 23.02.51@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/7b0c9024-aa79-4dfd-b1db-76adead3f651.png)

Allowを押すと、今後はSalesforce側でExternal Access認証の確認がありますので、Confirmします。
![CleanShot 2025-03-09 at 23.04.18@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/2450652e-51d7-43c0-a3f0-8511ebc3f976.png)

上記のPermission Setを個人に割り当てていないと以下のようなエラーが出ます。External Crednetial Principalへのアクセスが付与されていないからですね。Permission Setを先に先制してユーザーに割り当ててから行いましょう。
![CleanShot 2025-03-09 at 22.56.56@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/f7db4f2e-4163-4e25-8eb6-dd56e9f0c6e3.png)


# 動作確認
Salesforceにログインし、LWCを配置したページに移動します。
lightning-record-pickerでユーザーを選択し、選択されたUserのspaceIdを取得、メッセージを入力し、「Send Message」ボタンをクリックします。Google Chatにメッセージが送信されることを確認します。

# 追加要素
1. 追加として、v1/spacesで全てのグループチャットスペースも取得できます。そちらも発展系として利用可能です。
[spaces](https://developers.google.com/workspace/chat/api/reference/rest/v1/spaces)
1. API Callをなるべく少なくするには、カスタムオブジェクト（private）を作成して一度取得したspaceIdを登録しておき、参照するようにするとAPI callを節約できます。
1. MessageはCards v2スタイルで送るとより綺麗な通知になりますが、Chat Appから投稿するように設定する必要があるため、もう一手間かかります。
[Cards v2](https://developers.google.com/workspace/chat/api/reference/rest/v1/cards)
1. WebhookでもCards v2スタイルで送信できます。Webhookの場合は機能はシンプルですが、各ユーザーがWebhookの登録を行わないといけないので多くの従業員がいる場合はハードルが高いです。
1. [Salesforce x Google](https://cloud.google.com/blog/ja/products/gcp/salesforce-and-google-bring-gemini-to-agentforce-enable-more-customer-choice-in-major-partnership-expansion?hl=ja) Salesforce と Google、Gemini を Agentforce に導入を発表 パートナーシップ強化により顧客の選択肢をさらに拡大
とありましたね。 Salesforce x Slack x Google (gemini)と連携していくとより簡単な連携になっていくといいですね。

# まとめ
以上で、SalesforceからGoogle Chatへメッセージを送信する方法の紹介は終わりです。LWCとApexを組み合わせることで、簡単にGoogle Chatへのメッセージ送信を実現できます。ぜひ試してみてください。

[Brand element guidelines](https://about.google/brand-resource-center/brand-elements/)