こんにちは、CE課(コーポレートエンジニアリング課)の江利です。
Legendsじゃない方のApexの話です。
この記事ではいくつかあるSalesforceからSlackへ投稿する方法のうちのひとつとしてApexからSlackAPI経由する方法を紹介したいと思います。想定要件は下記となります。
- 既に実装済みのビジネスロジックにSlack通知を追加したい。
- Slack通知を追加する箇所が複数あり対象となる投稿先チャンネルが異なる。
自分で書いていてこんなこと言うのもアレですが、かなりピンポイントな要件ですね。とは言え既にかなりApexで作り込まれた組織であるが故にテストコードも大量に実装済みで、フローで実装してしまうとテスト実行時のDML処理がキックされて不要なSlack通知が発生したりテスト実行時にエラーが発生してしまうといったケースも起こり得ます。(実際に私はそんな事態に遭遇してしまったため今回の実装を行いました。)
リモートサイトの設定
ご利用中のSalesforce組織にSlackのAppExchangeがインストールされている場合はパッケージ内にリモートサイトの設定が含まれているためこちらの設定は不要となります。未インストールの場合は設定画面よりクイック検索にリモートサイトの設定
と入力し新規リモートサイトボタンをクリックします。今回はSlackに接続するので画像のように設定します。
SlackAppの設定
今回はApexからHttpRequestでchat.postMessageのメソッドをコールします。APIの詳細は公式のドキュメントを参照ください。
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を参照ください。
カスタムメタデータ型へレコードを登録
今回作成したカスタムメタデータ型の表示ラベルの横に「レコードの管理」のリンクがありますのでそちらをクリックします。新規ボタンをクリックし表示ラベルに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を参照ください。
続いて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部PE課でSalesforceエンジニアをやっています。Salesforce歴は13年くらいなのでチョットだけわかります。
記事への質問やフィードバックは yoshiaki.eri@serverworks.co.jp までお願いいたします。
Trailblazer プロファイル