hayase hayase

Webhook を利用して送信データを Salesforce に送る

フォームの Webhook 機能を利用して Salesforce と連携する方法を紹介します。
※ Webhook 機能は MovableType.net、MovableType.net フォーム共にスタンダードプラン以上で利用できます。

Salesforce 側の設定

Salesforceの操作等については詳しくはSalesforceのドキュメントをご覧ください。

カスタムオブジェクトの定義

最初にフォームのデータを保存するためのカスタムオブジェクトを定義します。

MovableType.net 側のフォームの「項目名」と、Salesforce側の「項目の表示ラベル」をあわせてください。項目数は一致している必要はなく、どちらかにしか無い項目があっても問題ありません。

Apex クラスを登録する

今回は以下の2つのApex クラスを登録します。

  • MTNetFormWebhook : MovableType.netのフォームからのwebフックを処理するクラスです
  • InquiryController : フォーム毎の、URLやカスタムオブジェクトを指定するクラスです

MTNetFormWebhook

global class MTNetFormWebhook {
  public class MTNetFormWebhookException extends Exception {
  }

  global static String post(String secret, Schema.sObjectType sObjectType) {
    String entryJson = RestContext.request.params.get('entry');
    Blob computedMac = Crypto.generateMac(
      'HmacSHA256',
      Blob.valueOf(entryJson),
      Blob.valueOf(secret)
    );

    if (
      RestContext.request.params.get('signature') !=
      EncodingUtil.convertToHex(computedMac)
    ) {
      throw new MTNetFormWebhookException('Invalid Signature');
    }

    sObject sobj = sObjectType.newSObject();
    List<ContentVersion> cvs = new List<ContentVersion>();

    Map<String, Object> entry = (Map<String, Object>) JSON.deserializeUntyped(
      entryJson
    );

    integer sequenceNumber = (integer) entry.get('sequence_number');
    sobj.put('Name', sequenceNumber.format());

    List<Object> columns = (List<Object>) entry.get('columns');

    Map<String, Schema.SObjectField> fmap = sObjectType.getDescribe()
      .fields.getMap();
    for (Schema.sObjectField f : fmap.values()) {
      Schema.DescribeFieldResult fd = f.getDescribe();

      for (Integer i = 0; i < columns.size(); i++) {
        Map<String, Object> column = (Map<String, Object>) columns[i];
        if (column.get('label') == fd.getLabel()) {
          sobj.put(
            fd.getName(),
            toSObjectValue(fd.getType(), (String) column.get('value'))
          );
        }
      }
    }

    for (Integer i = 0; i < columns.size(); i++) {
      Map<String, Object> column = (Map<String, Object>) columns[i];
      if (column.get('type') == 'file' && column.get('url') != '') {
        Http h = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint((String) column.get('url'));
        req.setMethod('GET');
        HttpResponse res = h.send(req);
        if (res.getStatusCode() != 200) {
          throw new MTNetFormWebhookException('Failed to download attachment.');
        }

        ContentVersion cv = new ContentVersion(
          Title = (String) column.get('label'),
          Description = (String) column.get('value'),
          PathOnClient = (String) column.get('value'),
          VersionData = res.getBodyAsBlob(),
          ContentLocation = 'S'
        );
        cvs.add(cv);
      }
    }

    insert sobj;

    for (ContentVersion cv : cvs) {
      insert cv;

      Id docId = [
        SELECT ContentDocumentId
        FROM ContentVersion
        WHERE Id = :cv.Id
      ]
      .ContentDocumentId;

      ContentDocumentLink cde = new ContentDocumentLink(
        ContentDocumentId = docId,
        LinkedEntityId = sobj.Id,
        ShareType = 'V'
      );
      insert cde;
    }

    return sobj.Id;
  }

  private static Object toSObjectValue(Schema.DisplayType type, String value) {
    switch on type {
      when Boolean {
        return value != '';
      }
      when Integer {
        return Integer.valueOf(value);
      }
      when Long {
        return Long.valueOf(value);
      }
      when MultiPicklist {
        return value.replace(',', ';');
      }
      when Date, DateTime {
        return Date.parse(value);
      }
      when else {
        return value;
      }
    }
  }
}

InquiryController

以下の内容の場合 「https://{ホスト名}/services/apexrest/inquiry」がwebフックのURLになります。 secretとsObjectTypeの値は、フォームとカスタムオブジェクトに応じて変更してください。

@RestResource(urlMapping='/inquiry')
global class InquiryController {
  private static String secret = 'secret-value';
  private static Schema.sObjectType sObjectType = Inquiry__c.sObjectType;

  @HttpPost
  global static String doPost() {
    return MTNetFormWebhook.post(secret, sObjectType);
  }
}

サイトを作成する

サイトおよびドメインのサイトからサイトを作成してください。

ApexクラスをREST APIで公開する

「公開アクセス設定」の中の「有効なApexクラス」にInquiryControllerを登録してください。

リモートサイトの設定を行う

フォームにファイル項目がある場合には、リモートサイトの設定で「 https://mt-net-form-assets.s3.ap-northeast-1.amazonaws.com 」へのアクセスを許可します。 (ファイル項目がない場合には設定の必要はありません。)

MovableType.net (フォーム)側の設定

Movabletype.net のフォームの基本設定画面で「データの保存」を「Webhook」にして「Payload URL」に URL を登録します。
(同じ手順で進めてきた場合には https://{ホスト名}/services/apexrest/inquiry というURLになっています。)
「secret」には、上の InquiryController で指定したものと同じ値を入力します。(例の場合は「secret-value」)。

フォームを送信する

フォーム上でデータを送信して、salesforceへデータが追加されるかを確認します。エラーが発生した場合はメールが届きます。

以上で Salesforce との連携設定は終了です。

フォームの Webhook 連携は様々なサービスと組み合わせて効率の良い運営が可能ですので是非ご活用ください。