SalesforceでApexからSlack投稿する共通クラスを作ってみた

記事タイトルとURLをコピーする

こんにちは、CE課(コーポレートエンジニアリング課)の江利です。

Legendsじゃない方のApexの話です。

この記事ではいくつかあるSalesforceからSlackへ投稿する方法のうちのひとつとしてApexからSlackAPI経由する方法を紹介したいと思います。想定要件は下記となります。

  • 既に実装済みのビジネスロジックにSlack通知を追加したい。
  • Slack通知を追加する箇所が複数あり対象となる投稿先チャンネルが異なる。

自分で書いていてこんなこと言うのもアレですが、かなりピンポイントな要件ですね。とは言え既にかなりApexで作り込まれた組織であるが故にテストコードも大量に実装済みで、フローで実装してしまうとテスト実行時のDML処理がキックされて不要なSlack通知が発生したりテスト実行時にエラーが発生してしまうといったケースも起こり得ます。(実際に私はそんな事態に遭遇してしまったため今回の実装を行いました。)

リモートサイトの設定

ご利用中のSalesforce組織にSlackのAppExchangeがインストールされている場合はパッケージ内にリモートサイトの設定が含まれているためこちらの設定は不要となります。未インストールの場合は設定画面よりクイック検索にリモートサイトの設定と入力し新規リモートサイトボタンをクリックします。今回はSlackに接続するので画像のように設定します。

SlackAppの設定

今回はApexからHttpRequestでchat.postMessageのメソッドをコールします。APIの詳細は公式のドキュメントを参照ください。

api.slack.com

Appを作成する

こちらのページからSlackワークスペースにログインしCreate New Appボタンをクリックします。 api.slack.com

今回はFrom scratchを選択します。App Nameにアプリ名を入力しアプリを作成するワークスペースを選択したらCreate Appをクリックします。

権限を付与する

次にOAuth & Permissionsより権限を設定します。ScopesのBot Token ScopesからAdd an OAuth Scopeをクリックし下記権限を付与します。

  • channels:read
  • chat:write
  • chat:write.customize
  • chat:write.public
  • groups:read
  • users:read
  • users:read.email

アプリ名を決める

App HomeからBotの名前を設定します。Your App’s Presence in SlackからApp Display NameのEditをクリックしDisplay Name (Bot Name)とDefault Nameをそれぞれ入力しSaveをクリックします。基本的には最初の工程で入力したApp Nameが入っているので変更不要であればスキップしてください。

Basic InformationよりInstall to Workspaceをクリックすると権限の確認画面に遷移するので許可するをクリックします。(念のため自身で設定した権限がきちんと付与されているかを確認しましょう。)

ちなみにここまでの作業をApp Manifestで実施する場合は下記JSON形式となります。

{
    "display_information": {
        "name": "ApexSample"
    },
    "features": {
        "bot_user": {
            "display_name": "ApexSample",
            "always_online": false
        }
    },
    "oauth_config": {
        "scopes": {
            "bot": [
                "channels:read",
                "chat:write",
                "chat:write.customize",
                "chat:write.public",
                "groups:read",
                "users:read",
                "users:read.email"
            ]
        }
    },
    "settings": {
        "org_deploy_enabled": false,
        "socket_mode_enabled": false,
        "token_rotation_enabled": false
    }
}

トークンの設定

クレデンシャルをApexクラスにハードコーディングするのはもちろん御法度ですのでトークンはカスタムメタデータに保持するようにしました。カスタムメタデータの良いところはDeveloper Sandboxであってもコピー時に情報がコピーされる点ですのでまだ使ったことのない方は是非活用してみてください。本番組織とsandbox組織でクレデンシャルが異なる場合などレコードを分けるといった方法で対処可能です。カスタムメタデータ型の利用が初めてではない方はこのセクションは飛ばしていただいて問題ありません。

カスタムメタデータ型を作成

ご利用のSalesforce組織でまだカスタムメタデータ型が未定義、もしくは今回を機に外部サービス連携で利用するクレデンシャル用のカスタムメタデータ型を定義する場合は、設定画面よりクイック検索にて「カスタムメタデータ型」で検索し新規カスタムメタデータ型をクリックします。 表示ラベルとオブジェクト名、必要に応じて説明を入力します。表示のラジオボタンは一番上のすべての Apex コードと API でこの種別を使用でき、[設定] で参照可能です。を選択してください。保存ボタンをクリックすれば新規カスタムメタデータ型の作成は完了です。

今回は表示ラベル名外部システムクレデンシャル、オブジェクト名はExternalSystemCredentialとします。

カスタムメタデータ型へカスタム項目を追加

カスタムメタデータ型へのカスタム項目追加はオブジェクトのカスタム項目作成と同じ要領です。カスタム項目セクションの新規ボタンをクリックします。今回はトークンを登録する項目となるためデータ型はテキストを選択し次へボタンをクリックします。項目の表示ラベルはトークンとし文字数は255文字、項目名はTokenとします。

sandbox組織用のクレデンシャルを1レコードで管理するか、項目で管理するかは悩ましいところですがカスタムメタデータ型の制限は組織あたり1,000万文字であり、Apexで取得する際に発行するSOQLはガバナ制限の対象外となるため*1正直好きな方を選んで問題ないと思います。ちなみに私はレコードを分ける方を選択しました。項目で分ける場合はTokenDevの様な項目を追加すると良いと思います。

カスタムメタデータ型の制限の詳細は公式のHelpを参照ください。

help.salesforce.com

カスタムメタデータ型へレコードを登録

今回作成したカスタムメタデータ型の表示ラベルの横に「レコードの管理」のリンクがありますのでそちらをクリックします。新規ボタンをクリックし表示ラベルにSlackAppトークン、外部システムクレデンシャル名にSlackAppToken、トークンにはSlackAppのOAuth & PermissionsからBot User OAuth Tokenの値をコピー&ペーストします。sandbox組織様にレコードを分ける場合は別途表示ラベルにSlackAppトークン(sandbox)、外部システムクレデンシャル名にSlackAppTokenSandboxの様に登録すると良いと思います。

これでトークンの設定は完了です。

投稿先チャンネル情報の設定

こちらもトークン同様にカスタムメタデータ型で管理するようにしました。表示ラベルSlack通知設定、オブジェクト名はSendSlackSettingとします。

今回私が管理対象とした項目は下記となります。

  • アイコン絵文字(IconEmoji__c)
    • :hogehoge:のようにワークスペースに登録された絵文字を指定します。
  • アプリ名(PostUserName__c)
  • 通知先チャンネル名(PostChannelName__c)
    • #hogehoge-channelのようにチャンネル名を記入します。

こちらの用途は後ほど詳しく説明します。

Apexでの実装内容

ではここからは実際にApexクラスに実装した内容を紹介していきます。Apexクラス名はSlackCall.clsとします。

まずは下記のような定数を定義しておきます。

private static final String CUSTOM_METADATA_DEVELOPER_NAME = 'SlackAppToken';
@TestVisible
private static Boolean isCalloutTest = false; // テスト実行時のcalloutを実行するかどうかのフラグ
@TestVisible
private static HttpResponse res; // テスト実行時にcalloutの結果を取得するためのクラス変数

次にDeveloperNameからSlack通知設定のカスタムメタデータレコードを取得するメソッドを用意します。

public static List<SendSlackSetting__mdt> fetchSendSlackSettings(String developerName) {
    List<SendSlackSetting__mdt> sendSlackSettings = [
        SELECT PostChannelName__c, PostUserName__c, IconEmoji__c
        FROM SendSlackSetting__mdt
        WHERE DeveloperName = :developerName
    ];
    if (sendSlackSettings.size() > 0) {
        return sendSlackSettings;
    } else {
        throw new SlackCallException('対象のカスタムメタデータを取得できませんでした。');
    }
}

SOQLでカスタムメタデータ型のレコードを取得する場合にはオブジェクトのAPI名はxxx__mdtの形式となります。基本的な構文は通常のsObjectのSOQLとほぼ同じ記法ですが一部制限がありますので詳細は公式Helpを参照ください。

help.salesforce.com

続いてDeveloperNameと投稿内容の本文からHttpRequestのURLを作成するメソッドを用意します。

public static String createUrl(String developerName, String messageText) {
    List<SendSlackSetting__mdt> sendSlackSettings = fetchSendSlackSettings(developerName);
    ExternalSystemCredential__mdt externalSystemCredential = [SELECT Token__c FROM ExternalSystemCredential__mdt WHERE DeveloperName = :CUSTOM_METADATA_DEVELOPER_NAME];
    PageReference pr = new System.PageReference('https://slack.com/api/chat.postMessage');
    pr.getParameters()
        .putAll(
            new Map<String, String>{
                'token' => externalSystemCredential.Token__c,
                'channel' => sendSlackSettings[0].PostChannelName__c,
                'username' => sendSlackSettings[0].PostUserName__c,
                'icon_emoji' => sendSlackSettings[0].IconEmoji__c,
                'text' => messageText
            }
        );
    return pr.getUrl();
}

ここで先程作成したSlack通知設定を使っています。今回は汎用的に利用するSalck投稿用の共通クラスのため通知先のチャンネルやアプリ名を用途によって変えたいという要件を満たすためにカスタムメタデータ型を利用しています。そうすることで後々変更があった場合にApexクラスではなくカスタムメタデータレコードを編集すれば済むためメンテナンスコストが格段に下がるためです。

最後に実際にHttpRequestを送信するメソッドです。

@future(callout=true)
public static void callWebhook(String sendSlackSettingDeveloperName, String messageText) {
    if (Test.isRunningTest() && !isCalloutTest) {
        // テスト実行時はcalloutされないため何もしない
        return;
    }
    String url = createUrl(sendSlackSettingDeveloperName, messageText);

    HttpRequest req = new HttpRequest();
    req.setEndpoint(url.substringBefore('?'));
    req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
    req.setMethod('POST');
    req.setBody(url.substringAfter('?'));

    Http http = new Http();
    try {
        res = http.send(req);

        System.debug(LoggingLevel.INFO, '@@@' + sendSlackSettingDeveloperName + 'HTTPResponse=' + res.toString());
        System.debug(LoggingLevel.INFO, 'STATUS:' + res.getStatus());
        System.debug(LoggingLevel.INFO, 'STATUS_CODE:' + res.getStatusCode());
    } catch (System.CalloutException ex) {
        System.debug(LoggingLevel.ERROR, '@@@callWebhook error message=' + ex.getMessage());
    }
}

テスト時の分岐ですが、このクラス自体の単体テスト以外では都度モックアップをセットするのが手間になってしまうのでテスト実行時で意図しない場合は以降の処理を無視するために入れています。デバッグメッセージについては共通クラスということで最低限の情報はログとして出力するようにしています。恒常的に出力するログであればログレベルを正しく設定することで開発者コンソールでフィルタリング出来るようなるのできちんと設定することを推奨します。これらは不要であれば削除してください。

@future(callout=true)アノテーションを付与している理由は多くの場合特定のレコードの特定の項目が更新された場合などにSlack通知したいケースであろうことからApexトリガでの実装となる想定であり、Apexトリガからコールアウト処理を実行する際には非同期処理である必要があるためです。

実際にこのクラスを呼び出す処理は下記のようなイメージです。

public static void noticeUpdateAccount(List<Account> accounts) {
    String sendSlackSettingDeveloperName = 'hogehoge';
    Boolean isSandbox = [SELECT Id, IsSandBox FROM Organization LIMIT 1].IsSandBox;
    if (isSandbox) {
        sendSlackSettingDeveloperName += 'Sandbox';
    }
    String message = 'hogehoge';
    SwxSlackCall.callWebhook(sendSlackSettingDeveloperName, message);
}

例として引数が取引先のリストとなっているのは取引先のトリガから本メソッドがコールされるためです。1回の処理で複数レコードが更新される場合などにSlack通知を1通にしたい場合などはfor文内で本文を作成し最後にまとめたものを通知するなどの工夫が必要になりますが今回は割愛させていただきます。

isSandboxの分岐についてですが私の場合はカスタムメタデータレコードを本番組織とsandbox組織とで分けた場合にDeveloperNameに'Sandbox'とサフィックスを付与したためです。こちらも不要であれば削除してください。

さいごに

今回はかなり長くなってしまったためテストコードについては別エントリーで紹介させていただきます。この記事がどなたかのSalesforceからApex経由でSlackに通知を送りたい要望解決への一助となればと思います。それでは良きSalesforceライフを!

*1:ロングテキストエリア項目を含むSOQLの場合はガバナ制限の対象となります

江利 義陽(記事一覧)

CE部CE課でSalesforceエンジニアをやっています。Salesforce歴は12年目くらいなのでチョットだけわかります。
記事への質問やフィードバックは yoshiaki.eri@serverworks.co.jp までお願いいたします。
Trailblazer プロファイル