[Salesforce] Chatter への投稿、返信を Slack に通知する(メンション、リンクも有効化する)

2021.12.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

当社では Salesforce から Slack に通知を飛ばすのに Salesforce to Slack を使っています。インストールすると Slack Message オブジェクトが組織に作成され、この Slack Message オブジェクトを作成すると slack に通知が飛ばせるというとても使いやすいアプリです。リードの作成や活動の作成をトリガーにして Slack Message オブジェクトを作成するプロセスなどを組むだけで簡単にSlack通知ができます。

先日、「 Chatter (Salesforce内で使用できる Twitter のようなメッセージングサービス) の投稿や返信の通知を Slack に飛ばせないか?」というリクエストがあって調査してみましたので共有したいと思います。

Chatter への投稿を Slack に通知する

私が調べた範囲では、Chatterの投稿と返信の両方をトリガーにして動作するプロセスやフローは定義できないようでした。しかし、Apexトリガーを使うことで投稿発生時、返信発生時の双方のタイミングで処理を起動することができました。

投稿発生時に起動させるApexトリガーの大枠は次の通りです。

trigger FeedItemTrigger on FeedItem (after insert) {
    // write something you want to do
}

返信発生時に起動させるApexトリガーの大枠は次の通りです。

trigger FeedCommentTrigger on FeedComment (after insert) {
    // write something you want to do
}

投稿は FeedItem に対して、返信は FeedComment に対してトリガーを仕掛けるだけです。 後は、このトリガーの中で Slack Message オブジェクトを作ってあげれば Slack に通知が飛ばせます。

Chatter のメンションをSlack通知にも反映させる

実は、今回の要件に「メンションが記載されていたら、そのユーザに対してSlackでもメンションを飛ばしたい」という要件もありました。 先に示したApexトリガーでは、 Trigger.new で作成された投稿、または返信を参照できますが、その本文情報を取得してそのまま Slack Message オブジェクトを作成すると、メンションやリンクが単なるテキストとして処理されて Slack に通知されてしまいます。

愚直に対応する方法としては、 Apex で正規表現を駆使してメンションやリンクを取り出し、 Slack の文法で評価されるように本文情報を書き直すということが考えられますが、どこまでがメンションか、リンクかといった判断は難しく、特に当社ではメンションに使われるユーザ名には「姓 名」と半角で区切ったフルネームを採用しているため、単純に「@以降から半角スペースまで」という判断ができませんでした。

それで、なんとか上手くメンション部やリンク部を取り出す方法がないかと調べたところ、 salesforce.stackexchange.com にそのものズバリの投稿を見つけました。

FeedItem Trigger - Parse Mentions?

ConnectApi.BatchResult[] を取得することで、投稿や返信の各セグメント(メンションやリンクやテキストやタグ)を本文に現れた順に取得することができ、そこに変換処理を差し込めば簡単に Slack の文法に書き換えられるのです。 投稿を解析して、Slack文法に書き換える Apex を下記に示します。

trigger FeedItemTrigger on FeedItem (after insert) {

    FeedTriggerHandler handler = new FeedTriggerHandler();

    String communityId = Network.getNetworkId();
    List<String> feedItemIds = new List<String>();
    for (FeedItem f : Trigger.new) {
        feedItemIds.add(f.id);
    }

    // Re-fetch the feed items passed into the trigger
    ConnectApi.BatchResult[] results = ConnectApi.ChatterFeeds.getFeedElementBatch(communityId, feedItemIds);

    for (ConnectApi.BatchResult result : results) {
        if (result.isSuccess()) {
            Object theResult = result.getResult();
            List<SlackMessage__c> messages = new List<SlackMessage__c>();
            if (theResult instanceof ConnectApi.FeedItem) {
                ConnectApi.FeedItem item = (ConnectApi.FeedItem) theResult;
                String messageBody = '';
                for (ConnectApi.MessageSegment segment : item.body.messageSegments) {
                    messageBody += handler.parseSegment(segment);
                }
                SlackMessage__c message = handler.makeSlackMessage(messageBody, '#some-channel', 'Chatter通知 by Apex');
                messages.add(message);
            }
            insert messages;
        }
    }
}

ConnectApi.ChatterFeeds.getFeedElementBatch メソッドで発生した全投稿分の ConnectApi.BatchResult[] を取得し、各投稿に対して、 ConnectApi.MessageSegment を取り出し、そのセグメントを FeedTriggerHandler.parseSegment メソッドに渡して、Slack通知用に変換をかけています。 FeedTriggerHandler は次のように実装しています。

public with sharing class FeedTriggerHandler {
    public FeedTriggerHandler() {
    }

    public SlackMessage__c makeSlackMessage(String body, String channel, String bot) {
        SlackMessage__c message = new SlackMessage__c();
        message.Text__c = body;
        message.Channel__c = channel;
        message.Username__c = bot;
        return message;
    }

    public String parseSegment(ConnectApi.MessageSegment segment) {
        String messageBody = '';
        if (segment instanceof ConnectApi.TextSegment) {
            ConnectApi.TextSegment theText = (ConnectApi.TextSegment) segment;
            String text = theText.text;
            messageBody += text + '\n';
        }
        if (segment instanceof ConnectApi.MentionSegment) {
            ConnectApi.MentionSegment theMention = (ConnectApi.MentionSegment) segment;
            String mentionedId = theMention.record.id;
            // Do what you need to do with mentionedId...
            System.debug('Mentioned ID: ' + mentionedId);
            User mentionUser = [SELECT Id, SlackID__c FROM User WHERE Id = :mentionedId LIMIT 1];
            messageBody += '<@' + mentionUser.SlackID__c + '>';
        }
        if (segment instanceof ConnectApi.LinkSegment) {
            ConnectApi.LinkSegment theLink = (ConnectApi.LinkSegment) segment;
            String url = theLink.url;
            messageBody += '<' + url + '>';
        }
        return messageBody;
    }
}

parseSegment メソッドでセグメント( ConnectApi.MessageSegment )を受け取り、そのセグメントの種別(テキストか、メンションか、リンクか)で処理を分け、それぞれのセグメントの要素が Slack でも評価されるように変換をかけています。

テキストセグメントは '\n' を末尾に追加し、メンションセグメントではメンション対象のレコードIDが ConnectAPI.MentionSegment.record.id で得られるので、そのレコードIDでユーザオブジェクトを検索して、当該ユーザの Slack ID を取得して <@Slack ID> のフォーマットに変換し、リンクセグメントではURLを <URL> のフォーマットに変換しています。1

これらの変換によって、Slackで改行、メンション、リンクが反映されるSlack文法にのっとった本文を生成しています。

後は、 makeSlackMessage メソッドで Slack Message オブジェクトを作り、 insert して実際にレコードを作成しています。

なお、 Chatter の返信の場合は ConnectApi.ChatterFeeds.getFeedElementBatch ではなく、下記のように ConnectApi.ChatterFeeds.getCommentBatch メソッドを使います。

trigger FeedCommentTrigger on FeedComment (after insert) {
    FeedTriggerHandler handler = new FeedTriggerHandler();

    String communityId = Network.getNetworkId();
    List<String> feedCommentIds = new List<String>();
    for (FeedComment f : Trigger.new) {
        feedCommentIds.add(f.id);
    }

    ConnectApi.BatchResult[] results = ConnectApi.ChatterFeeds.getCommentBatch(communityId, feedCommentIds);

    for (ConnectApi.BatchResult result : results) {
        if (result.isSuccess()) {
            Object theResult = result.getResult();
            List<SlackMessage__c> messages = new List<SlackMessage__c>();
            if (theResult instanceof ConnectApi.Comment) {
                ConnectApi.Comment comment = (ConnectApi.Comment) theResult;
                String messageBody = '';
                for (ConnectApi.MessageSegment segment : comment.body.messageSegments) {
                    messageBody += handler.parseSegment(segment);
                }
                SlackMessage__c message = handler.makeSlackMessage(messageBody, '#some-channel', 'Chatter通知 by Apex');
                messages.add(message);
            }
            insert messages;
        }
    }
}

セグメントの解析方法は同じなので、 FeedTriggerHandler にまとめています。

結果確認

下記のように、投稿でもその返信でも Slack に通知が飛び、メンションが有効になっていることが確認できました。

投稿。

Chatter での投稿テスト

投稿のSlack通知。

投稿のSlack通知結果

返信。

Chatter での返信テスト

返信のSlack通知。

返信のSlack通知結果


  1. 当社ではユーザオブジェクトに各ユーザの Slack ID をカスタムフィールドを追加して持たせています。