2016-04-11

LINE BOT APIをforce.comで試してみた

巷で話題のLINE BOT APIをforce.comで試してみましたー

事前準備

LINE BOT APIの設定に関しては割愛しますが(参考URL参照)、メッセージリクエストのホワイトリスト登録では、SalesforceのIPアドレスホワイトリストが広いので面倒です。全部設定するのは無理だと思うので、検証用であれば以下のようなIPアドレスのエラーをわざと発生させて、forceのIPアドレスを取得してそれをホワイトリストに登録すれば通ると思います(もちろん変更する可能性は大いにあるので一時的な検証用として利用する感じで)。
{
  "statusCode": "427",
  "statusMessage": "Your ip address [136.147.62.8] is not allowed to access this API."
}

どうしても無理なら、APIコールの本体を固定IPを持ったサーバに持たせて、プロキシ的にリクエストすればOKです。

メッセージ送信

ソースコードはこんな感じで。trialbot-api.line.meに対するリモートサイトの設定もお忘れなく。
public with sharing class LineBotClient {
    private static final String LINE_BOT_EVENT_URI = 'https://trialbot-api.line.me/v1/events';
    private static final String EVENT_TYPE = '138311608800106203';
    private static final String TO_CHANNEL = '1383378250';
    
    private String channelId;
    private String channelSecret;
    private String channelMID;

    public LineBotClient(String channelId, String channelSecret, String channelMID) {
        this.channelId = channelId;
        this.channelSecret = channelSecret;
        this.channelMID = channelMID;
    }
    
    public String sendMessage(Set<String> mids, Map<String, Object> content) {
        HttpRequest req = new HttpRequest();
        req.setHeader('Content-type', 'application/json; charset=UTF-8');
        req.setHeader('X-Line-ChannelID', this.channelId);
        req.setHeader('X-Line-ChannelSecret', this.channelSecret);
        req.setHeader('X-Line-Trusted-User-With-ACL', this.channelMID);
        
        String jsonBody = Json.serialize(new Map<String, Object> {
            'to' => mids,
            'toChannel' => TO_CHANNEL,
            'eventType' => EVENT_TYPE,
            'content' => content
        });
        req.setHeader('Content-Length', String.valueOf(jsonBody.length()));
        
        req.setEndpoint(LINE_BOT_EVENT_URI);
        req.setMethod('POST');
        
        req.setBody(jsonBody);
        
        Http http = new Http();
        HTTPResponse res = http.send(req);
        return res.getBody();
    }
    
    public String sendTextMessage(Set<String> mids, String message) {
        return this.sendMessage(mids, new Map<String, Object>{
            'contentType' => '1',
            'toType' => '1',
            'text' => message
        });
    }
    
    public String sendImageMessage(Set<String> mids, String originalContentUrl, String previewImageUrl) {
        return this.sendMessage(mids, new Map<String, Object>{
            'contentType' => '2',
            'toType' => '1',
            'originalContentUrl' => originalContentUrl,
            'previewImageUrl' => previewImageUrl
        });
    }
    
    public String sendVideoMessage(Set<String> mids, String originalContentUrl, String previewImageUrl) {
        return this.sendMessage(mids, new Map<String, Object>{
            'contentType' => '3',
            'toType' => '1',
            'originalContentUrl' => originalContentUrl,
            'previewImageUrl' => previewImageUrl
        });    
    }
    
    public String sendAudioMessage(Set<String> mids, String originalContentUrl, Integer audlen) {
        return this.sendMessage(mids, new Map<String, Object>{
            'contentType' => '4',
            'toType' => '1',
            'originalContentUrl' => originalContentUrl,
            'contentMetadata' => new Map<String, Object>{
                'AUDLEN' => String.valueOf(audlen)
            }
        });
    }
}

ちなみにApex RESTは470のステータスコードを返却できないので"Unable to process the contents of the received request."をLINEサーバに伝達できません。

コールバックのクラス

コールバックには任意のヘッダを入れることができず、Authorizationヘッダによる認証ができないので、公式サポートされてるのかイマイチ不明なAnonymous Apex RESTを利用します。具体的な実装方法に関してはこちらの記事を参照してください。コールバックURLは以下のようになります。

https://{サイトのドメイン}:443/services/apexrest/line_callback

クラスはこんな感じで作りました ※乃木坂仕様です

@RestResource(urlMapping='/line_callback')
global with sharing class LineBotCallback {
    private static final String channelID = 'input your ChannelID';
    private static final String channelSecret = 'input your ChannelSecret';
    private static final String channelMID = 'input your ChannelMID';

    @httpPost
    global static Map<String, String> doPost() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;
        
        Map<String, Object> callbackParams = (Map<String, Object>)JSON.deserializeUntyped(req.requestBody.ToString());
        List<Object> resultParams = (List<Object>)callbackParams.get('result');
        String calculatedSignature = 
            EncodingUtil.base64Encode(Crypto.generateMac('HmacSha256', req.requestBody, Blob.valueOf(channelSecret)));
        
        String signature = req.headers.get('X-Line-ChannelSignature');
        if (calculatedSignature != signature) {
            res.statusCode = 401;
            return new Map<String, String> {
                'result' => 'failure',
                'description' => 'Error: Calculated signature is not different\n\ncalculated signature:'
                                     + calculatedSignature + '\nrequest signature:' + signature
            };
        }
        
        LineBotClient client = new LineBotClient(channelId, channelSecret, channelMID);
        for (Object obj : resultParams) {
            Map<String, Object> params = (Map<String, Object>)obj;
            Map<String, Object> contentParams = (Map<String, Object>)params.get('content');
            String message = (String)contentParams.get('text') ;
            Set<String> mids = new Set<String>{ (String)contentParams.get('from') };
            
            String debugBody = '';
            if (message == '超絶可愛い!') {
                debugBody = client.sendTextMessage(mids, 'まいやーん!');
            } else if (message == '白石麻衣') {
            	String maiyanImageUrl = '{まいやんの画像URL}';
                debugBody = client.sendImageMessage(mids, maiyanImageUrl, maiyanImageUrl);
            }
            System.debug(debugBody);
        }
        return new Map<String, String>{
            'result' => 'success'
        };
    }
}

メッセージ配信自体はfutureやbatchを使って非同期処理させた方が良いかもです。

で、このBOTとお友達になってやりとりするとこんな感じになります↓

line-maiyan

その他

コールバックのデバッグするときはrunscope等のプロキシを噛ますと楽かも。 runscopeを使う場合、サイトのドメインがhoge-fuga.force.comであれば ``` https://hoge--fuga-force-com-{runscope_bucket}.runscope.net:443/services/apexrest/line_callback ``` というようにコールバックURLを書き換えればOK(ハイフンはハイフン2つ、ドットはハイフンにそれぞれ変換)

また、MIDはユーザ自身が設定するIDとは異なるので、ユーザからのメッセージに含まれるcontent.fromを参照して適宜取得する必要があります。

参考URL

所感

事前準備で書いた通り、SalesforceのIPアドレスのレンジが広いので、24-30というサブネットの仕様が緩和されない限り、実運用でSalesforceから直接メッセージを飛ばすのは厳しそうです。またAnonymous Apex RESTも非公式な匂いがプンプンするので、forceからLINE BOTをあれこれしたい場合は、HerokuなりAWSなりでサーバ立てて、プロキシ&Webhookしてもらうのが現状は良さそうな感じがします。force.comのキュー(及びワーカー)の仕組みもちょっと弱い感じなので。

Salesforceと連携するとなると顧客とのチャネルに利用する話だと思うので、ケースでメール送信の代わりにLINEを使うとかそんな感じになるんですかねー。

このエントリーをはてなブックマークに追加