Sodech Developer Blog

株式会社ソデック技術ブログ

Salesforce 未検証ユーザーへの検証メール自動送信処理

1.記事を書いた理由

突然ですが皆さんはユーザーの検証はおこなっていますか?

Summer'24 のメジャーリリースの際に未検証ユーザーからメールを送信することができなくなりました。

今後、ユーザーからメールを送るためにはユーザーを検証済みにする必要があります。

▼メール検証要件を満たすためのメールアドレスの検証 https://help.salesforce.com/s/articleView?id=release-notes.rn_security_email_verification.htm&release=250&type=5

私の担当しているプロジェクトで未検証のユーザーを確認したところ、一部のユーザーが未検証のままでした。プロジェクトではMFA(多要素認証)対応としてSSO(シングルサインオン)を導入しており、未検証ユーザーが増えていたのかなと予想しています。

このままではApexやフローなどで未検証のユーザーからメールアラートが送れなくなってしまう点と、セキュリティ的にも脆弱だと考え、ユーザーへ検証メールを送ることにしました。

未検証ユーザーの数がかなり多いため、Apexバッチで一括送信を行いました。

この記事では複数のユーザーへ一括してメールを送るための手順を紹介します。

2.この記事の対象者

・Salesforceの管理者をしている方

・未検証ユーザーが多く、検証メールを一括で送信したい方

・定期的に未検証ユーザーへ検証メールを送信したい方

3.ユーザーのメールアドレス検証

ユーザーのメールアドレスを検証するにはいくつかの方法がありますが、ここでは2つ紹介します。

1.システム管理者がユーザーへ検証メールを送信する

[設定] > [ユーザー] を押下する。 該当のユーザーを選択してメール横の[検証]リンクを押下する。 スクリーンショット 2024-10-12 15.02.20.png

メール文中のリンクを押下する。 スクリーンショット 2024-10-12 15.19.18.png

2.ユーザー自身で登録しているメールアドレス宛に検証メールを送信する

[設定] > [右上のアイコン] > [設定] > [高度なユーザーの設定] を押下する。 該当のユーザーを選択してメール横の[検証]リンクを押下する。 スクリーンショット 2024-10-12 14.33.34.png

▼ユーザのメールアドレスの検証 https://help.salesforce.com/s/articleView?id=release-notes.rn_security_verify_user_email_addresses.htm&release=244&type=5

3.未検証ユーザーの抽出

未検証ユーザーを抽出するには TwoFactorMethodsInfo オブジェクトを使用して、UserId 項目、HasUserVerifiedEmailAddress 項目を取得します。 HasUserVerifiedEmailAddress 項目はユーザーが未検証の場合に false を返すので、それらのユーザーのUserIdを取ってくることで未検証ユーザーを抽出できます。

SELECT UserId, HasUserVerifiedEmailAddress FROM TwoFactorMethodsInfo WHERE HasUserVerifiedEmailAddress = 'false'

サポートされているコールが describeSObjects()、query() のため、Salesforce inspector で Export はできないので注意してください。

▼オブジェクトリファレンス[TwoFactorMethodsInfoオブジェクト] https://developer.salesforce.com/docs/atlas.ja-jp.object_reference.meta/object_reference/sforce_api_objects_twofactormethodsinfo.htm

4.メール送信クラス

Apexからメール送信を行うには UserManagementクラスを使用します。sendAsyncEmailConfirmationメソッドはパラメーターでメールテンプレートやExperience Cloud サイトの情報を受取ることができます。

パラメーター 説明
userId 確認メールを受信するユーザーの ID
emailTemplateId 検証リンクが定義されているメールテンプレートの ID
networkId Experience Cloud サイトの ID
startUrl ユーザーは検証後にこのページにリダイレクトされ、パラメーターとして成功メッセージまたはエラーメッセージが渡されます。null の場合、ユーザーはログインページにリダイレクトされます。

▼Salesforce 経由でメールを送信するためにメールアドレスを確認する

https://help.salesforce.com/s/articleView?id=release-notes.rn_security_email_verification.htm&release=246&type=5

▼非同期メールでメールアドレスを確認する

https://help.salesforce.com/s/articleView?id=sf.emailadmin_async_email_verification.htm&type=5

5.メール送信クラスのサンプル

public class SendVerifyEmail implements Database.Batchable<sObject>{

    public SendVerifyEmail() {
    }

    public Database.QueryLocator start(Database.BatchableContext bc) {

        // 対象ユーザーの取得
        String strSql = '   SELECT Id' +
                        '        , Name' +
                        '        , UserName' +
                        '     FROM User' +
                        '    WHERE IsActive = true';

        return(Database.getQueryLocator(strSql));

    }

    public void execute(Database.BatchableContext bc,  List<User> activeUserList) {

            List<TwoFactorMethodsInfo> emailNotVerifiedUserList = [
                SELECT UserId
                FROM TwoFactorMethodsInfo
                WHERE HasUserVerifiedEmailAddress = false
                AND UserId IN :activeUserIds
            ];

            // 検証メール送信
            for(TwoFactorMethodsInfo emailNotVerifiedUser : emailNotVerifiedUserList) {
                System.UserManagement.sendAsyncEmailConfirmation(emailNotVerifiedUser.UserId, null, null, null);
            }

    }

    public void finish(Database.BatchableContext bc) {
        //メール送信処理終了
    }

}

6.テストクラスのサンプル

@isTest
public class SendVerifyEmailTest {
    
    @isTest
    static void SendVerifyEmailTest() {
        // テストデータの作成
        User unverifiedUser = new User(
            Username = 'testuser001@example.com',
            Email = 'testuser002@example.com',
            ProfileId = [SELECT Id FROM Profile WHERE Name = 'システム管理者' LIMIT 1].Id,
            IsActive = true
        );
        insert unverifiedUser;
    }
}

7.まとめ

いかがでしたでしょうか。 未検証ユーザーへ一度に検証メールを送信したい場合には是非活用してみてください。

【Salesforce】レコードに添付したファイルのダウンロードリンクを生成する方法

0.投稿の動機

添付ファイルに関連するオブジェクトは意外に多いです。それらのオブジェクトがどのような機能を持っているのかイマイチ理解しにくいと感じている方々の助けになればと思い、本記事を投稿させていただきました。

1.今回実現したいこと

①ナレッジ記事に添付されたファイルのダウンロードリンクを生成する

②Experience Cloud サイト上でリンクをクリックすると、ファイルがダウンロードされる

おおまかにこの2点となります。

2.添付ファイルに関連するオブジェクト一覧

表示ラベル オブジェクト名 説明
コンテンツドキュメント ContentDocument 組織にアップロードしたファイルの情報が格納
コンテンツドキュメントリンク ContentDocumentLink ファイルと紐づくユーザー、レコード等の情報が格納
コンテンツバージョン ContentVersion ファイルのバージョン情報が格納
コンテンツ配信 ContentDistribution ファイルの外部共有に関する情報が格納

特に「コンテンツドキュメントリンク」と「コンテンツ配信」は肝となるオブジェクトなので、説明していきます。

2-1.コンテンツドキュメントリンク

押さえておきたいポイントは2つあります。

  • 組織にファイルをアップロードした時と、レコードにファイルを添付した時にコンテンツドキュメントリンクレコードが作成される

つまり、ファイルがレコードに添付された状態だと、1つのファイルに対しコンテンツドキュメントリンクのレコードは2つ存在することになります。(上段が組織にファイルをアップロードした時、下段がレコードにファイルを添付した時に作成されたもの)

  • ”LinkedEntityId”にファイルが添付されているレコードのIDが格納されている

LinkedEntityIdにはファイルをアップロードしたユーザーのIDや、ファイルが添付されているレコードのIDが格納されます。上記の画像の場合、「005…」はユーザーID、「ka0…」はレコードID(今回はknowledgeレコード)を表しています。

2-2.コンテンツ配信

このオブジェクトでダウンロードリンクを生成されます。しかし、ダウンロードリンクにも種類がありますので、用途に応じて使い分ける必要があります。

表示ラベル API参照名 機能
ファイルのダウンロードリンク ContentDownloadUrl ファイルをダウンロードするリンク。クリックすると直ダウンロードされる。
外部リンク DistributionPublicUrl 共有ドキュメントへのリンク。クリックするとプレビュー画面に遷移する。

プレビュー画面

今回は、上段の「ファイルのダウンロードリンク」を使用して、リンクをクリックしたら即座にダウンロードされるようにします。

 

その他にも細かい設定内容がありますが、それについては次章の中でその都度説明します。

それでは、早速取り掛かっていきましょう!

3.添付されたファイルのダウンロードリンクを生成する

3-1.カスタム項目を作成する

ダウンロードリンクを記述するためのカスタム項目を作成します。

データ型は”リッチテキストエリア”を選択、その他は任意の値を設定します。

3-2.フローを作成する

今回はアクションボタンを押下後リンクを生成したいので、画面フローを作成していきます。長丁場になりますが、よろしくお願いしますm(__)m

まずは下記の画像のところまでのフローを作成します。

①変数「recordId」を作成

この変数は、フローを実行しているレコードのIDを自動で格納してくれる便利アイテムです!

②コンテンツドキュメントリンクを取得

絞り込み条件には

項目 演算子 値
LinkedEntityId 次の文字列と一致する recordId

と設定します。

これにより、ボタンをクリックしたレコードに添付されたファイルに関連するコンテンツドキュメントリンクの情報が取得できます。

③コンテンツドキュメントリンクの取得有無を判定(任意)

私は今回レコードにファイルが添付されていない場合「なし」という文言を表示させたかったので、このような分岐を加えています(作成手順は省略)。

取得されたレコードが存在するという条件は

リソース 演算子 値
②のコンテンツドキュメントリンク取得要素 null False

と設定します。

 

以下は取得されたレコードが存在した場合の処理を記述しています。

 

④コンテンツバージョン取得

コンテンツ配信作成にはコンテンツバージョンIDが必要です。

ここでは絞り込み条件を設定せず、全コンテンツバージョンレコードを取得します。

⑤取得したコンテンツドキュメントリンクをループ

コレクション変数は②のコンテンツドキュメントリンク取得要素を選択します。

⑥取得したコンテンツバージョンを絞り込む

取得したコンテンツバージョンを、ループしているコンテンツドキュメントリンクが持つContentDocumentIdで検索します。

検索条件は

項目 演算子 値
ContentDocumentId 次の文字列と一致する ⑤のループ要素.ContentDocument.Id

と設定します。

⑦検索したコンテンツバージョンを格納するためのレコードコレクション変数を作成

⑧検索したコンテンツバージョンを格納

変数値は

変数 演算子 値
⑦の変数 追加 ⑥のコレクション検索条件要素

と設定します。

これで使用するコンテンツバージョンのレコードを取得することができました。

下記画像までの続きを作成していきます。

⑨取得したコンテンツバージョンをループ

コレクション変数は⑦の変数を選択します。

⑩コンテンツ配信作成用の情報を格納するためのレコード(単一)変数作成

⑪⑩の変数を格納するためのレコードコレクション変数作成

⑫コンテンツ配信作成用の情報を格納

コンテンツ配信レコードを作成するための情報を格納していきます。

⑩の変数に、作成するために必要な情報を格納し、ループ終了後に一括作成するために⑪のコレクション変数に⑩の変数を格納しているという流れです。

変数値は

変数 演算子 値
ContentVersionId 次の文字列と一致する ⑨のループ要素.Id
有効期限(ExpiryDate) 次の文字列と一致する 任意の日付(今回は空欄)
コンテンツ配信名(Name) 次の文字列と一致する ⑨のループ要素.Id.Title
ブラウザーでの参照を許可(PreferencesAllowViewInBrowser) 次の文字列と一致する True
最初の参照またはダウンロードされたら私に通知(PreferencesNotifyOnVisit) 次の文字列と一致する TrueもしくはFalse(今回はFalse)
⑪のコレクション変数 追加 ⑩の変数

に設定します。

「最初の参照またはダウンロードされたら私に通知」は、true に設定すると、初めて表示またはダウンロードされたときにファイルの所有者にメールで通知されます。今回は通知する必要はないため、Falseに設定しています。

メール通知内容

⑬コンテンツ配信を作成

レコードコレクションは⑪のコレクション変数を選択します。

無事にコンテンツ配信を作成することができました。

続きのフローを作成していきます。

 

⑭コンテンツ配信を取得する

⑮作成したコンテンツ配信をループ

コレクション変数は⑪のコレクション変数を選択します。

⑯取得したコンテンツ配信を絞り込む

取得したコンテンツ配信を、ループしているコンテンツ配信のIDで検索します。

検索条件は

項目 演算子 値
Id 次の文字列と一致する ⑮のループ要素..Id

と設定します。

⑰検索したコンテンツ配信を格納するためのレコードコレクション変数を作成

⑱検索したコンテンツ配信を格納

変数値は

変数 演算子 値
⑰の変数 追加 ⑯のコレクション検索条件要素

と設定します。

作成されたコンテンツ配信の情報を取得することができました。

続きのフローを作成します。ラストスパートです!

⑲取得したコンテンツ配信をループ

コレクション変数は⑰のコレクション変数を選択します。

⑳リンクを記述するテキストテンプレートを作成

少し手順が複雑なので、順を追って説明します。

「リソースを挿入…」から⑲の変数を選択します。

コンテンツ配信名を選択します。

改行して、同じようにファイルのダウンロードリンクを選択します。

赤枠内を選択し、切り取ります。

コンテンツ配信名(Name)リソースを全選択後、右下のリンクマークをクリックします。

リンクURLに先程切り取った文字列を貼り付け保存します。

完了をクリックします。

これでテキストテンプレートの作成は完了です。

㉑テキストテンプレートを格納するテキスト変数を作成

㉒テキストテンプレートの文言を格納

変数値は

変数 演算子 値
㉑の変数 追加 ⑳のテキストテンプレート

と設定します。

㉓Knowledgeレコードを更新

絞り込み条件には

項目 演算子 値
Id 次の文字列と一致する recordId

と設定します。

「項目値をレコードに設定」では、項目には作成したカスタム項目、値には㉑の変数を設定します。

以上でフロー作成は終了です。保存し有効化しましょう!

3-3.アクションボタンを設定する

以下の手順で、アクションボタン設定画面まで遷移します。

設定>オブジェクトマネージャー>任意のオブジェクト>ボタン、リンク、およびアクション>新規アクション

下記画像のように設定し、保存します。

ページレイアウトに設置するのも忘れずに行いましょう!

以上で、添付されたファイルのダウンロードリンクを生成する実装は完了です。

レコード詳細画面上で生成されたリンクをクリックすると、ダウンロードされると思います!この状態であれば、Experience Cloud サイト上でも同じ挙動になりますので、今回実現したいことを叶えることができました。

お疲れさまでした!

4.最後に

今回は、レコードに添付したファイルのダウンロードリンクを生成する方法を紹介しました。

冒頭にも書きましたが、添付ファイルに関連するオブジェクトが複数存在しており、それぞれがどのようなデータを持っているのかなどを把握するのに苦労しました。

リンクをクリックしたらファイルがダウンロードされるという機能はよく見かけるものなので、ぜひこちらの記事を参考にしていただければ幸いです。

長くなってしまいましたが、最後までお付き合いくださりありがとうございました。

【Salesforce×OPROARTS】Salesforce ApexクラスでOPROARTS APIにHTTPリクエスト送信してみた

0. 投稿の動機

OPROARTSが提供しているAPIを利用することで、SalesforceのApexクラスやLWC上から帳票の作成やメール送信の実装をすることが可能であることを知りました。実装方法について悩んでいる方の助けになればと思い、本記事を投稿しようと思います。

本記事では、SalesforceからOPROARTS APIを利用する方法を具体的に解説し、同様の実装を考えている方の参考となるよう詳しく説明します。

1. 読者の対象

・OPROARTSの帳票デザインツールの操作方法を理解している

・OPROARTSが発行するVFページによる帳票発行ではなく、カスタマイズされた帳票発行の実装を考えている

・OPROARTS APIの存在は知っているが、Salesforceでの具体的な実装方法が分からない

2. OPROARTSとは

OPROARTS(オプロアーツ)は、帳票を電子化するためのクラウド型電子帳票システムです。
電子帳票の作成だけではなく、作成した帳票の出力側の活用、そして既に存在している紙書類やExcel帳票からテキストデータを取り出し、上位システムへ格納する入力側の活用まで紙業務関するお客様の業務をサポートすることができます。

3. Salesforce上から外部APIにHTTPリクエスト送信する方法

Salesforceから外部APIにリクエストする場合、LWCやAuraといった画面コンポーネントから「Fetch API」などを利用してHTTPリクエストすることができません。

その為、Salesforceから外部のAPIにリクエストする場合はApexクラスを介してHttpRequest送信を実行する必要があります。

Apexクラスでの外部APIリクエストの実装には「HttpRequest」クラスを使用します。レスポンスは「HttpResponse」クラスのインスタンスとして受け取ります。

 

図3-1:構文(SalesforceのApexクラスでのHttpRequest送信の例)

4. Apexクラス上でのOPROARTS APIのHTTPリクエスト送信の実装

図3-1は、SalesforceのApexクラスから外部APIにHTTPリクエストを送信する際の基本構文です。これを利用して、OPROARTS APIへのリクエスト送信が可能になります。

この構文が崩れることはないので、あとは「エンドポイントURL」、「メソッド」「コンテントタイプ」「リクエストBody」を設定することでOPROARTS APIに対してHTTPリクエスト送信を実行することが可能になります。

OPROARTS APIに対してHTTPリクエストする際の値については以下の表のようになります。

 

表4-1:OPROARTS APIに対してHTTPリクエストする際の値

項目 値
エンドポイントURL

https://www.oproarts.com/Facade

※プランによって「Facade」が使用できない場合があるのでその場合はサポートへの問合せが必要です

メソッド POST

リクエストHeader:

 コンテントタイプ

application/x-www-form-urlencoded
リクエストBody ※設定するパラメータは次のセクションで説明します

『表4-1』にある通り、リクエストBodyに必要なパラメータについては次のセクションで解説します。

『表4-1』をHttpRequestにセットし、HTTPリクエスト送信を実行することでOPROARTS APIを実行することが可能になります。

5. OPROARTS APIに必要なパラメータについて

ここではOPROARTS APIのリクエストBodyに含めるパラメータについて説明します。

まずは基本のパラメータについて説明します。基本パラメータは以下になります。

 

表5-1:OPROARTS APIに送信するリクエストBodyの基本パラメータ

パラメータ 意味 説明 備考
CID 顧客ID OPROARTS契約時に発行される出力(LA)用のCID 必須
UID ユーザID OPROARTS契約時に発行される出力(LA)用のUID 必須
UPW ユーザパスワード OPROARTS契約時に発行される出力(LA)用のUPW 必須
OP 操作指示

下記のいずれかを指定する

ここではPDFで出力する場合の操作指示のみ説明します
dlp : pdfドキュメントのダウンロード
mlp : pdfドキュメントの添付されたメールを送信
  ※「メール送信時のパラメータ」の設定が必要

必須
TP 帳票テンプレート名

帳票デザインが定義されたファイルの名前を指定する

※マルチテンプレートの場合はTP1, TP2, TP3, ・・・, TPn : のように複数指定する

必須
TD テキストデータセット

帳票に表示するテキストデータ(CSV)の内容を指定する

CSV形式なので、各データをカンマつなぎで一つの文字列として設定する

(例:'A,B,C')

複数のテキストデータセットを指定する場合は、TD1, TD2, TD3, ・・・, TDnのように指定する

※マルチテンプレートの場合は、TD1_1, TD1_2, TD2_1, TD2_2, TD2_3, TD3_1, ・・・, TDn_m のように複数指定する

例えば、「TD2_1」は、「TP2で指定したテンプレートの、1番目のテキストデータセット」という意味になる

データの中にカンマや改行がある場合、荒けじめ別の文字列に置換しておく必要がある

⇒OPROARTSデザイナ側で置換された文字列を元に戻す設定を実施する

『表5-1』にある「OP」で「mlp」を指定した場合、メール送信時のパラメータの設定が必要になります。メール送信時のパラメータについては『表5-2』を参考にしてください。

 

表5-2:OPでメール送信の操作を設定した場合のメール送信用パラメータ

パラメータ 意味 説明 備考
MT メール送信先

メールの送信先アドレスを指定する

複数の送信先を使用する場合、各アドレスをカンマ区切りで指定する

必須
MCC メール送信先(CC) メールの送信先アドレス(CC)を指定する

複数の送信先(CC)を使用する場合、各アドレスをカンマ区切りで指定する

 
MBC メール送信先(BCC) メールの送信先アドレス(BCC)を指定する

複数の送信先(BCC)を使用する場合、各アドレスをカンマ区切りで指定する

 
MF メール送信元

メールの送信元アドレスを指定する

必須
MSJ メール件名

メールの件名を指定する

 
MB メール本文

メールの本文を指定する

 

MA 添付ファイル名

添付ファイル名を指定する

生成されたドキュメントの添付ファイル名になります

※省略された場合、システム管理されているファイル名が付与される

 

これでHTTPリクエストに必要な「エンドポイントURL」、「メソッド」「コンテントタイプ」「リクエストBody」についての解説が終了しました。

次に、OPROARTS APIをSalesforceのApexクラスからHTTPリクエスト送信する際の注意点について解説していきます。

6. OPROARTS APIをSalesforceのApexクラスからHTTPリクエスト送信する際の注意点

SalesforceのApexクラスからOPROARTS APIにHTTPリクエスト送信を実行する際に、いくつか注意することがあります。

以下の点に注意して実装してください。

注意1:OPROARTSデザイナで作成する帳票テンプレートの型は「Live」にする必要がある

・Live型のテンプレートはCSVデータで帳票上にデータをマッピングするので、OPROARTS APIを利用することが可能

・Live for Salesforce型だとSalesforce上の項目をマッピングすることになるのでOPROARTS APIが利用できなくなってしまう

注意2:OPROARTS APIへのHTTPリクエスト送信後のレスポンスHeaderの「Location」へのリダイレクト処理が必要

・単純にOPROARTS APIにHTTPリクエスト送信するだけでは正常に実行できません

・レスポンスHeaderにある「Location」をエンドポイントに指定する

・レスポンスHeaderにある「Set-Cookie」をリダイレクト用のリクエストHeaderに指定する

・「メソッド」を"Get"、「コンテントタイプ」を"application/json;charset=UTF-8"に指定する

・上記内容でリダイレクトを実行する

注意3:OPROARTS APIのリクエスト送信時に指定した「OP」が"dlp"の場合、注意1を実行した後に返ってくるレスポンスBodyは文字化けしないよう、Blob型で受け取る必要があります。

・レスポンスBodyから帳票(PDF)を受け取れるが、そのままStringで受け取ると文字化けしてしまう

・レスポンスBodyをBlob型で受け取り、PDFとしてデータを取得する必要がある

 (HttpResponseのインスタンスに対して<.getBodyAsBlob>メソッドを使用してBlob型で受け取る)

注意4:Apexクラスから外部APIにHTTPリクエスト送信する際には、その実行前にDML操作(insert/update/delete)を実行してはいけない

・「System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out」というエラーが発生し、HTTPリクエスト送信が失敗します

7. 実装例(帳票(PDF)が添付されたメールの送信)

それでは実際にOPROARTS APIをSalesforceのApexクラスからHTTPリクエスト送信を実行してみましょう。

※OPROARTSの帳票デザインツールの使用方法と、項目マッピングについて理解している前提で説明します。

まず初めに次のような帳票があるとします。

 

図7-1:テンプレートのデザイン(テンプレート名「SampleQuoteDocument」)

青枠で説明しるものはこれからパラメータTDで設定したテキストデータ(CSV)をマッピングしていく箇所です。

次に『図7-2』~『図7-5』でCSVデータと、そのデータの帳票上へのマッピングを設定しています。

 

図7-2:TD1にあたるCSV定義

図7-3:テンプレートへのマッピング(TD1)

図7-4:TD2にあたるCSV定義

図7-5:テンプレートへのマッピング(TD2)

『図7-2』と『図7-4』にある「先頭行を無視する」のチェックは外しておきましょう。

『図7-3』と『図7-5』ではマッピング上で数式を利用しています。これは数値のフォーマットの設定や、受け取ったCSVデータに対して文字をつけ足したりなどの設定をしています。一番のポイントとしては改行が含まれるであろう項目では予め改行が別の文字列に変換されたことを想定して、その変換された文字を改行に戻すという数式の設定があるというところです。

本例でいうと、「備考」の部分が改行がある可能性があるので、予め改行は「;」に変換されたことを想定して数式の設定をしています。

そして最後にCSVデータのマッピングを配備したときのドキュメント生成情報です。

 

図7-6:ドキュメント生成情報

『図7-6』の赤枠で囲われたところに注目してください。これらはOPROARTS APIのリクエストBodyに設定するパラメータになります。ここで必要な値や実際に設定する内容を把握することが可能です。

それではApexクラス上でOPROARTS APIへのHTTPリクエスト送信のコーディング例を見ていきます。ここでは帳票(PDF)が添付されたメールの送信の実装例になります。

 

Apexクラスコーディング例

/**
 * QPRARTS APIへのHTTPリクエスト送信サンプルクラス
 */
public without sharing class CreateSampleQuoteDocumentController {

    // メイン処理(見積書作成)
    public void sendQuoteDocumentSampleMethod() {

        try {

            // テンプレートに設定するデータの作成 (TD項目)
            String td1 = createCsv_1();
            String td2 = createCsv_2();

            // リクエストbody
            String body = '';

            // CID、UID、UPN、TP1の設定(パラメータ設定)
            body = body + 'CID=XXXXXXX&'; // CID
            body = body + 'UID=XXXXXXXXXXX&'; // UID
            body = body + 'UPN=XXXXXXXXXXX&'; // UPN
            body = body + 'TP1=Sample_SampleQuoteDocument'; // TP1

            // メディアタイプの設定
            body = body + '&TD1=' + td1; // TD1
            body = body + '&TD2=' + td2; // TD2

            // 添付ファイル(証明書)の設定
            body = body + '&H1=XXXXX@seedhocoh.com'; // H1 (証明書)
            body = body + '&H2=xxxxx@xxxx.co.jp'; // H2 (証明書)
            body = body + '&H5=xxxx@xxx.com'; // H5 (証明書)
            body = body + '&H6=XXX区役所'; // H6 (区役所)
            body = body + '&AA=XXXXXX'; // AA (添付ファイル名)

            // Request URL
            String url = 'https://www.qprarts.com/facade';

            // HTTPリクエスト作成
            HttpRequest request = new HttpRequest();
            request.setEndpoint(url);
            request.setMethod('POST');
            request.setHeader('Content-Type', 'application/x-www-form-urlencoded');
            request.setBody(body);

            // HTTPオブジェクト
            Http http = new Http();
            HttpResponse response = http.send(request);

            // リダイレクトステータスコード判定
            if (response.getStatusCode() == 200 || response.getStatusCode() == 302) {
                String location = response.getHeader('Location');
                String cookie = response.getHeader('Set-Cookie');
                HttpRequest request2 = new HttpRequest();
                request2.setEndpoint(location);
                request2.setMethod('GET');
                request2.setHeader('Content-Type', 'application/json;charset=UTF-8');
                request2.setHeader('Cookie', cookie);

                HttpResponse response2 = http.send(request2);

                // HTTPリクエストに失敗した場合に例外処理
                if (response2.getStatusCode() != 200) {
                    throw new AuraHandledException('HTTPリクエストに失敗しました');
                }
            }

        } catch(Exception e) {
            // HTTPリクエスト失敗時のcatch処理
            System.debug('HTTP Request failed');
            System.debug(e.getMessage());
        }
    }

    /**
     * TD1のパラメータデータ作成
     */
    private String createCsv_1() {
        Datetime dt = Datetime.now();
        String retStr = '';
        retStr = retStr + dt.format('yyyy年MM月dd日');
        retStr = retStr + 'XXXXXXXXXXXXX';
        retStr = retStr + 'XXXXXXXXXXXXX';
        retStr = retStr + 'XX'; // 項目データの設定
        retStr = retStr + 'こんにちは。山田です。'; // メッセージ設定
        retStr = retStr + 'XXXXXX'; // 追加メッセージの設定
        retStr = retStr + '東京市役所'; // 場所設定
        retStr = retStr + 'XXXXXX'; // その他

        return retStr;
    }

    /**
     * TD2のパラメータデータ作成
     */
    private String createCsv_2() {
        String retStr = '';
        for (Integer i = 1; i <= 3; i++) {
            retStr = retStr + '項目' + i + ':';
            retStr = retStr + 'XXXXXX';
            retStr = retStr + 'XXXXX';
            retStr = retStr + '1000';
        }
        retStr = retStr + 'XXXXX';
        retStr = retStr + 'XXXXX';

        return retStr;
    }
}

 

それでは開発者コンソールで実際にHTTPリクエスト送信メソッドを実行してみましょう。

 

図7-8:実行画面

実行に成功し、メール送信に成功しました。

パラメータで設定した内容でメール送信と帳票が発行されているのが確認できます。

 

図7-9:メールの確認

図7-10:添付されている帳票

今回の例ではTD2がデータテーブルになっているので、複数のデータを渡す必要があったので、TD2のテキストデータ作成で各行を改行コードで分割しています。

備考の改行もされていますね。

8. 最後に

これまでSalesforceのApexクラスからOPROARTS APIへのHTTPリクエスト送信の実装について説明してきましたが、Apex以外の言語でも、基本的なリクエストパラメータは変わりません。他の言語でOPROARTS APIを活用する際にも、本記事を参考にしていただければ幸いです。

【Salesforce】ApexクラスからのSlack通知実装方法

0. 投稿の動機

Salesforceに用意されているフローを使用したSlack通知ではなく、ApexクラスからSlack APIを呼び出して通知を送信する方法について学んだので、その実装方法の共有をしたいと思い本投稿を作成しました。

1. 読者の対象

・Apexクラス、LWCの実装経験のある方

・Apexクラスを使用してSlack通知を実装したい

2. 設定手順

以下の手順に従って設定をします。

① Slack ワークスペースとチャネルの作成(Slack側の設定)

② Slackアプリケーションの作成(Slack側の設定)

③ リモートサイトの設定(Salesforce側の設定)

④ Salesforce上に「Webhook URL」「チャネルID」を持たせる(Salesforce側の設定)

⑤ Slack通知を実行するApexクラスの作成(Salesforce側の設定)

2-①. Slack ワークスペースとチャネルの作成(Slack側の設定)

Slackワークスペースの作成とApexクラスから通知を受け取るためのチャネルを作成します。

図2-1

作成したチャネルを右クリックし、「チャネルの詳細を表示する」をクリックします。

表示されたチャネルの詳細にある「チャネルID」をどこかにメモしておきます。

図2-2

また、Slackユーザの「メンバーID」もメモしておく。

図2-3

2-②. Slackアプリケーションの作成(Slack側の設定)

Slack API 画面でアプリケーションを作成します。

「Create an App」ボタンをクリックし、アプリケーションを作成します。

図2-4

次の画面で「From scratch」を選択します。

図2-5

「App Name」にこれから作成するアプリケーション名を入力します。

(※「App Name」は半角英数字のみで設定する必要があることに注意する)

「Pick a workspace to develop your app in」では 2-① で作成したワークスペースを選択します。

上記2つの入力が完了したら「Create App」ボタンをクリックしてアプリケーションを作成します。

図2-6

次にメニューにある「Incoming Webhooks」を選択し、「Activate Incoming Webhooks」を有効化します。

図2-7

有効化した後画面をスクロールし、「Add New Webhook to Workspace」をクリックします。

図2-8

2-① で作成したSlack通知を受け取るためのチャネルを選択し、「許可する」ボタンをクリックします。

図2-9

生成された Webhook URL をどこかにメモしておきます。

図2-10

2-③. リモートサイトの設定(Salesforce側の設定)

Salesforceにログインし、設定画面を開きます。クイック検索に「リモートサイトの設定」を入力し、「リモートサイト設定」画面を開きます。

図2-11


「リモートサイト設定」画面にある「新規リモートサイト」ボタンをクリックします。「リモートサイト名」と「リモートサイトのURL」に「https://hooks.slack.com」を入力し、「保存」ボタンをクリックして設定内容を保存します。

図2-12

2-④. Salesforce上に「Webhook URL」「チャネルID」「メンバーID」を持たせる(Salesforce側の設定)

2-①でメモした「チャネルID」「メンバーID」と2-②でメモした「Webhook URL」をSalesforce上に持たせる。

今回の例では「メンバーID」はユーザーオブジェクトに保持し、「チャネルID」と「Webhook URL」はカスタムメタデータで保持する実装をしています。

したの画像の様に、ユーザーオブジェクトに「メンバーID」を設定できる項目を用意し、2-①でメモした「メンバーID」を入力して保存します。

図2-13

カスタムメタデータを作成し、「チャネルID」と「Webhook URL」を設定するための項目を用意します。

図2-14

メモしておいた「チャネルID」と「Webhook URL」をカスタムメタデータに登録します。

図2-15

1. 読者の対象

・Apexクラスを使用してSlack通知を実装したい

・Apexクラスのコーディングがある程度できる人

2. 設定手順

以下の手順に従って設定をします。

① Slack ワークスペースとチャネルの作成(Slack側の設定)

② Slackアプリケーションの作成(Slack側の設定)

③ リモートサイトの設定(Salesforce側の設定)

④ Salesforce上に「Webhook URL」「チャネルID」を持たせる(Salesforce側の設定)

⑤ Slack通知を実行するApexクラスの作成(Salesforce側の設定)

2-①. Slack ワークスペースとチャネルの作成(Slack側の設定)

Slackワークスペースの作成とApexクラスから通知を受け取るためのチャネルを作成します。

図2-16

作成したチャネルを右クリックし、「チャネルの詳細を表示する」をクリックします。

表示されたチャネルの詳細にある「チャネルID」をどこかにメモしておきます。

図2-17

また、Slackユーザの「メンバーID」もメモしておく。

図2-18

2-②. Slackアプリケーションの作成(Slack側の設定)

Slack API 画面でアプリケーションを作成します。

「Create an App」ボタンをクリックし、アプリケーションを作成します。

図2-19

次の画面で「From scratch」を選択します。

図2-20

「App Name」にこれから作成するアプリケーション名を入力します。

(※「App Name」は半角英数字のみで設定する必要があることに注意する)

「Pick a workspace to develop your app in」では 2-① で作成したワークスペースを選択します。

上記2つの入力が完了したら「Create App」ボタンをクリックしてアプリケーションを作成します。

図2-21

次にメニューにある「Incoming Webhooks」を選択し、「Activate Incoming Webhooks」を有効化します。

図2-22

有効化した後に画面をスクロールし、「Add New Webhook to Workspace」をクリックします。

図2-23

2-① で作成したSlack通知を受け取るためのチャネルを選択し、「許可する」ボタンをクリックします。

図2-24

生成された Webhook URL をどこかにメモしておきます。

図2-25

2-③. リモートサイトの設定(Salesforce側の設定)

Salesforceにログインし、設定画面を開きます。クイック検索に「リモートサイトの設定」を入力し、「リモートサイト設定」画面を開きます。

図2-26


「リモートサイト設定」画面にある「新規リモートサイト」ボタンをクリックします。「リモートサイト名」と「リモートサイトのURL」に「https://hooks.slack.com」を入力し、「保存」ボタンをクリックして設定内容を保存します。

図2-27

2-⑤. Slack通知を実行するApexクラスの作成(Salesforce側の設定)

次の様なLWCコンポーネント(slackSendMsgCmp)とApexクラス(SlackSendMsgCmpController.cls)を作成します。

 

SlackSendMsgCmpController.cls

public without sharing class SlackSendMsgCmpController {

    @AuraEnabled
    public static Map<String, String> sendToSlack(
        String slackMessage, // Slack通知メッセージ
        Boolean addChannelMention, // チャネルへのメンション付与フラグ
        Boolean addUserlMention, // メンバーへのメンション付与フラグ
        String objectApiName, // レコード詳細ページオブジェクトAPI参照名
        String recordId // レコード詳細レコードID
    ){
        // 実行結果格納Map
        Map<String, String> resultMap = new Map<String, String>();
        try {
            // soql文
            String soql = 'SELECT Id, Name'
                        + ' FROM ' + objectApiName
                        + ' WHERE Id = :recordId';

            // レコードの取得
            sObject redord = Database.query(soql);

            // レコードName
            String redord_name = String.valueOf(redord.get('Name'));

            // レコードID
            String redord_id = String.valueOf(redord.get('Id'));

            // Slack通知メッセージの生成(メンションやレコード詳細ページのリンクの追加)
            String message = generateMessage(slackMessage
                                           , addChannelMention
                                           , addUserlMention
                                           , redord_name
                                           , redord_id
                                           , objectApiName);
           
            // Slackカスタムメタデータの取得
            Slack__mdt slack = [SELECT ChannelId__c
                                     , WebhookUrl__c
                                  FROM Slack__mdt
                                 WHERE DeveloperName = 'SlackTest'];
           
            // Requestボディに乗せるパラメータの生成
            Map<String, String> paramMap = new Map<String, String>();
            paramMap.put('channel', slack.ChannelId__c);
            paramMap.put('text', message);
           
            // Slackメッセージ送信実行
            sendMessage(paramMap, slack.WebhookUrl__c);

            // 実行結果格納
            resultMap.put('title', 'Success!');
            resultMap.put('message', 'Slackメッセージ送信に成功しました。');
            resultMap.put('variant', 'success');
            return resultMap;

        } catch (Exception e) {

            // エラー内容格納
            resultMap.put('title', 'Failed...');
            resultMap.put('message', e.getMessage());
            resultMap.put('variant', 'error');

            return resultMap;
        }
    }

    /**
     * Slack通知メッセージの生成(メンションやレコード詳細ページのリンクの追加)
     */
    private static String generateMessage(
        String slackMessage,
        Boolean addChannelMention,
        Boolean addUserlMention,
        String redordName,
        String redordId,
        String objectApiName
    ){
        String retMsg = '';
        // チャネルメンション追加
        if(addChannelMention){
            retMsg = retMsg + '<!channel>' + '\r\n';
        }
        // メンバーメンション追加
        if(addUserlMention){
            // SlackメンバーIDの取得
            User targetUser = [SELECT Id
                                    , SlackMemberId__c
                                 FROM User
                                WHERE Id = :UserInfo.getUserId()];
            retMsg = retMsg + '<@' + targetUser.SlackMemberId__c + '>' + '\r\n';
        }
        // メッセージの追加
        if(!String.isBlank(slackMessage)){
            retMsg = retMsg + slackMessage;
        }
        // レコード詳細リンクの付与
        retMsg = retMsg
               + '\r\n'
               + '\r\n'
               + createRecordPageUrl(objectApiName, redordId, redordName);
        return retMsg;
    }

    /**
     * レコード詳細ページURL生成
     */
    private static String createRecordPageUrl(String objectApiName
                                            , String recordId
                                            , String linkName){
        String url = URL.getOrgDomainUrl()
                   + '/lightning/r/'
                   + objectApiName
                   + '/'
                   + recordId
                   + '/view';
        return '<' + url + '|' + linkName + '>';
    }

    /**
     * Slack通知Request送信
     */
    @future (callout=true)
    private static void sendMessage(
        Map<String, String> paramMap,
        String webHookUrl
    ) {
        Http http = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint(webHookUrl);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(paramMap));
        HttpResponse res = http.send(req);
    }
}

 

slackSendMsgCmp.js-meta.xml

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

 

slackSendMsgCmp.html

<template>
  <lightning-card title="Slackメッセージ送信">
    <div
      class="slds-p-around_medium" style="background-color: white;">
      <div>
        <lightning-textarea
          label="メッセージ内容"
          name="slackMessage"
          value={slackMessage}
          onchange={handleChange}
          required="true"
        ></lightning-textarea>
      </div>
      <div class="slds-m-top_small" >
        <p>
          <lightning-input
            type="checkbox"
            label="チャネルメンション追加"
            name="addChannelMention"
            checked={addChannelMention}
            onchange={handleChange}
          ></lightning-input>
        </p>
        <p>
          <lightning-input
            type="checkbox"
            label="ユーザーメンション追加"
            name="addUserlMention"
            checked={addUserlMention}
            onchange={handleChange}
          ></lightning-input>
        </p>
      </div>
      <div class="slds-m-top_small" style="display: flex; justify-content: end;">
        <lightning-button
          variant="brand"
          label="送信"
          name="send"
          onclick={handleClick}
          disabled={isDisabled}
        ></lightning-button>
      </div>
    </div>
  </lightning-card>
</template>

 

slackSendMsgCmp.js

import { LightningElement, api, track } from 'lwc';
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import sendToSlack from '@salesforce/apex/SlackSendMsgCmpController.sendToSlack'

/**
 * Slack通知コンポーネント
 */
export default class SlackSendMsgCmp extends LightningElement {
  @api recordId;
  @api objectApiName;
  @track slackMessage;
  @track addChannelMention;
  @track addUserlMention;

  /**
   * onchangeイベント
   * @param {*} event
   */
  handleChange = (event) => {
    const name = event.target.name;
    const value = event.target.value;
    const checked = event.target.checked;
    if(name === 'slackMessage'){
      this.slackMessage = value;
    }
    if(name === 'addChannelMention'){
      this.addChannelMention = checked;
    }
    if(name === 'addUserlMention'){
      this.addUserlMention = checked;
    }
  }

  /**
   * onclickイベント
   * @param {*} event
   */
  handleClick = async(event) => {
    const name = event.target.name;
    if(name === 'send'){
      await this.send();
    }
  }

  /**
   * Slack通知実行
   */
  send = async() => {
    let title = '';
    let message = '';
    let variant = '';
    await sendToSlack({
      slackMessage: this.slackMessage,
      addChannelMention: this.addChannelMention,
      addUserlMention: this.addUserlMention,
      objectApiName: this.objectApiName,
      recordId: this.recordId,
    })
    .then(res => {
      title = res['title'];
      message = res['message'];
      variant = res['variant'];
    })
    .catch(err => {
      title = 'Failed...';
      message = err.message;
      variant = 'error';
    })
    .finally(() => {
      this.setDefault();
      const evt = new ShowToastEvent({
        title: title,
        message: message,
        variant: variant,
      });
      this.dispatchEvent(evt);
    })
  }

  /**
   * 入力値初期化
   */
  setDefault = () => {
    this.slackMessage = '';
    this.addChannelMention = false;
    this.addUserlMention = false;
  }

  /**
   * 送信ボタン活性・非活性制御
   */
  get isDisabled(){
    return !this.slackMessage;
  }
}

3. 実際にメッセージを送信してみる

ステップ2で作成したコンポーネントをレコード詳細ページに配備します。 本記事では取引先責任者に配備しています。 コンポーネントではチャネルへのメンション、メンバーへのメンションの追加が可能で、メッセージの後にレコード詳細ページのURLが自動で付与される仕様になっています。

図3-1

図3-2のようにメッセージ内容を入力し、チャネル、メンバーへのメンションを追加します。入力が完了したら「送信」ボタンをクリックして実際にSlackチャネルへメッセージを送信します。

図3-2

メッセージの送信に成功しました。

図3-3

Slackのチャネルを確認するとメッセージが送信されていることが確認できました。

図3-4

図3-5を見るとメンションが正しく機能していることが確認できます。また、メッセージにある「山田 花子」のリンクをクリックするとSalesforceの取引先責任者レコードページが表示されました(図3-6)。これでリンクが正しく機能していることも確認できました。

図3-5

図3-6

4. 最後に

私個人の感想としてはSalesforceのApexクラスを使用してSlackのメッセージ送信の機能を実装をすること自体は簡単にできるなと思いました。

ただ、実際にSalesforceを使用する方にとってどんなUI・UXが良いのかについては別途学習が必要だと感じました。

今回はメンション、メッセージの送信、リンクの付与のみの実装でしたが、今後はファイルの添付などもできるか試してみたいと思っています。

最後まで見ていただいきありがとうございました。

 

【社内イベント】24期・全社キックオフミーティングを行いました!

全社キックオフミーティングイメージ


8月23日(金)に、全社キックオフミーティングを開催しました。
弊社では、毎年全社員が集まり、新年度の目標と計画を共有する全社キックオフミーティングを開催しております。

 

2023年度の振り返りと2024年度の施策の発表

続きを読む

【社内イベント】2023年忘年会開催しました!

こんにちは!先日、弊社の忘年会が盛大に行われました。
今年一年の労をねぎらい、仕事仲間たちと楽しいひとときを過ごすこのイベントは、毎年恒例の楽しみでもあります。

今年も会場は貸し会議室を借りて開催しました!私は当日準備チームとして、少し前に会場入りして、机などのレイアウトを変えるなどの準備しました。

いよいよ開始時刻となると続々メンバーが入場してきて、みんなで飲み物を片手に、上司の猪木風の乾杯からの・・・いざ開催「1、2、3,ダーーーーーー!」

乾杯の瞬間

乾杯の瞬間

仕事の話から離れて、みんなで笑い合いながら乾杯することで、普段とは違う一体感が生まれました。

今回の食事は、おにぎり、お寿司、ピザにケータリングを準備しました。ケータリングはケータリングサービスをしているお客様がいらっしゃるので、利用させて頂きました!

一緒に食事をとりながら、各テーブルで笑顔が広がります。メンバー同士の距離が縮まり、普段知りえない一面や「一人暮らしはじめます!」や「ラインスタンプ作ってみたい」など、新たな発見が楽しいひとときとなりました。

そして、恒例のビンゴ大会の景品は特別豪華となりました!なんと!ルンバです!強運な人たちは素敵な景品を手に入れ、更なる盛り上がりを見せました。

景品もらったHappyなメンバーの皆さん

景品をもらったHappyなメンバーの皆さん

今回の忘年会も新たな一年への期待が高まりました。社長の締めの言葉と一本締めで、無事に終えることができました。笑顔に満ちた忘年会は、これから会社を大きくしていく仲間との絆を深める素晴らしい機会となりました。

忘年会を準備してくれたメンバーへ感謝

続きを読む