LINE WORKSでタイ語自動翻訳機能を使う

# はじめに
LINE WORKSのトーク（チャット）機能は自動翻訳機能が付いています！
対応している言語は下記の通り。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/05a3d09e-4297-0031-7dc6-db4c13dff4ae.png)


自動翻訳機能をオンにするとこんな感じで、翻訳された文章が下に付与されて、文章が投稿されます。
私は毎日仕事でこの機能を使っています。むしろこれがないと無理w
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/a9f82e66-a469-442d-4465-43f35be3d581.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/79715e4d-760e-8f9a-ef71-46ae60b83b69.png)

ですが、LINEの普及が進んでいる「タイ語」にまだ対応していません・・・。
ないものは作るよね、ってことでタイ語翻訳Botを作りました。

# 完成版
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/a83d93f3-c0e2-07d1-0e6b-51027f754c24.png)

# 構成

BotサーバーはHerokuを使います。
言語はNode.jsです。
翻訳APIは、Microsoft　Cognitive ServiceのTranslator Text APIを使います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/23cbb291-2869-b742-8b92-bf1dd8b03e6a.png)

# 構築の流れ

構築の流れは下記のStepになります。
１、２、４、５は「LINE WORKSで初めてのBot開発！」という記事で詳しく紹介しているので、そちらでご確認ください。
リンクを貼っておきます。

1. [LINE WORKS　Developer Consoleの設定](https://qiita.com/tokotan/items/f615f4a62219d655436f#developer-console%E8%A8%AD%E5%AE%9A)
2. [Botサーバーの用意](https://qiita.com/tokotan/items/f615f4a62219d655436f#bot%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%AE%E7%94%A8%E6%84%8F)
3. Botプログラミング
4. [Herokuへデプロイ](https://qiita.com/tokotan/items/976d35ca56132e0bb5c1#heroku%E3%81%B8%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)
5. [Botを公開](https://qiita.com/tokotan/items/976d35ca56132e0bb5c1#bot%E5%88%A9%E7%94%A8%E8%A8%AD%E5%AE%9A)

ここからは、「3.Botプログラミング」に付いて書きます。

# Botプログラミング
Bot本体のメイン動作は**index.js**に、
Translater Text APIを使った翻訳機能は**tranlator.js**に実装します。
※わかりやすいように簡易的な構成にしています。

## Bot本体(index.js)

### モジュールのインストール
下記モジュールを追加でインストールします。

```
$ npm install express --save
$ npm install body-parser --save
$ npm install jsonwebtoken --save
$ npm install https --save
$ npm install request --save
$ npm install querystring --save
$ npm install crypto --save
```

index.jsにて、モジュールをインポートしておきます。

```index.js
//モジュールインポート
const express = require("express");
const bodyParser = require("body-parser");
const jwt = require('jsonwebtoken');
const https = require("https");
const request = require("request");
const qs = require("querystring");
const crypto = require("crypto"); //暗号化モジュール
const translate = require('./translate.js');//翻訳モジュール
```

### 環境変数設定

Server TokenやConsumer Keyは、Herokuの環境変数へ設定します。
Herokuの環境変数への設定はHerokuのダッシュボードから設定することができます。手順は[こちら](https://devcenter.heroku.com/articles/config-vars#using-the-heroku-dashboard)を参照してください。
今回は、下記５つの値をHerokuの環境変数として設定しましょう。

|環境変数名|Developer Consoleの表示箇所|
|--------|-------------------------|
|APIID|[API]メニューのAPI IDに表示されている英数字|
|CONSUMERKEY|[API]メニューのServer API Consumer KeyのKey欄に表示されている英数字|
|SERVERID|[API]メニューのServer List(ID登録タイプ)のID欄に表示されている英数字|
|PRIVATEKEY|[API]メニューのServer List(ID登録タイプ)の[認証キー再発行]ボタンからダウンロードします。<br> ダウンロードしたファイルに表示されている英数字
|BOTNO|[Bot]メニューに表示されているBotをクリックし、<br>[Bot No.]に表示されている４桁の数字|

全て設定するとこんな感じになります。

設定したら、プログラムから利用できるよう変数として定義しておきましょう。

```index.js
const APIID = process.env.APIID;
const SERVERID = process.env.SERVERID;
const CONSUMERKEY = process.env.CONSUMERKEY;
const PRIVATEKEY = process.env.PRIVATEKEY;
const BOTNO = process.env.BOTNO;
```

### ユーザーのメッセージを受け取りメッセージに改ざんがないか確認する

メッセージはPOSTリクエストのbody 部分に含まれて通知されます。（メッセージのプロパティについて、詳細は[こちら](https://developers.worksmobile.com/jp/document/100500901?lang=ja)を参照してください。）

この時に受信したメッセージが、本当にLINE WORKSのサーバーから送信されたメッセージであり、そのメッセージが改ざんされていないかどうか、を確認するようにしてください。
LINE WORKS メッセージサーバーから送信されたメッセージの改ざん有無を確認するプロセスは以下となります。
1. API ID を秘密鍵として利用し、LINE WORKSのサーバーから送られてきたメッセージの body の内容を HMAC-SHA256 アルゴリズムでエンコードします。
2. 上記の HMAC-SHA256 アルゴリズムでエンコードした結果を BASE64 エンコードします。
3. X-WORKS-Signature のヘッダー値と比較し、同一であればメッセージは改ざんされていないと判断できます。

確認のプロセスについて詳細は[こちら](https://developers.worksmobile.com/jp/document/1005009/v1?lang=ja)を参照してください。
そして、このプロセスは下記のように実装できます。
下記サンプルコードではcryptoというモジュールを使っています。

```index.js
//メッセージ改ざん有無チェック
const signature = req.headers['x-works-signature'];
if (signature){
    checkCallbackMessage(JSON.stringify(req.body), signature, (callback) => {
        if (callback){
            //正常処理をここに書く
        } else {
            console.log("LINE WORKSサーバー以外からのCallbackがありました。");
        };
    });
}
：
：
//LINE WORKSメッセージサーバーから送信されたメッセージの改ざん有無を確認
function checkCallbackMessage(httpRequestBody, signature, callback){
    //API ID を秘密鍵として利用し、LINE WORKSのサーバーから送られてきたメッセージの body の内容を HMAC-SHA256 アルゴリズムでエンコードします。
    // 暗号化
    const sha256 = crypto.createHmac('sha256', API_ID);
    //上記の HMAC-SHA256 アルゴリズムでエンコードした結果を BASE64 エンコードします。
    sha256.update(httpRequestBody);
    const hash = sha256.digest('base64');

    //X-WORKS-Signature のヘッダー値と比較し、同一であればメッセージは改ざんされていないと判断できます。
    if (hash == signature){
        callback(true);
    } else {
        callback(false);
    }
}
```
### メッセージをユーザーへ送信するためのトークンを取得する

トークンは、下記の流れで取得します。

1.JWTを取得
2.JWTを使ってServer Tokenを取得

それぞれ実装のサンプルコードは下記となります。詳細な仕様についてはこちらを参照してください。

```index.js
    getJWT((jwttoken) => {
        getServerToken(jwttoken, (newtoken) => {
            //取得したnewtokenを使ってメッセージ送信
        });
    });

//1. JWTを取得 
function getJWT(callback){
    const iss = SERVER_ID;
    const iat = Math.floor(Date.now() / 1000);
    const exp = iat + (60 * 60);　//JWTの有効期間は1時間
    const cert = PRIVATEKEY;
    const token = [];
    const jwttoken = jwt.sign({"iss":iss, "iat":iat, "exp":exp}, cert, {algorithm:"RS256"}, (err, jwttoken) => {
        if (!err) {
            callback(jwttoken);
        } else {
            console.log(err);
        }
    });
}
//2.JWTを使ってServer Tokenを取得
function getServerToken(jwttoken, callback) {
    const postdata = {
        url: 'https://authapi.worksmobile.com/b/' + API_ID + '/server/token',
        headers : {
            'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8',
        },
        form: {
            "grant_type" : encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer"),
            "assertion" : jwttoken
        }
    };
    request.post(postdata, (error, response, body) => {
        if (error) {
            console.log(error);
            callback(error);
        } else {
            const jsonobj = JSON.parse(body);
            const AccessToken = jsonobj.access_token;
            callback(AccessToken);
        }
    });
}
```

>**[注意]**
Server Token（access_token）は24時間以内に利用されない場合には自動的に失効します。access_token発行時に、発行時間とaccess_tokenを保存し、Server APIを実行する際には有効期限を確認するようにしてください。有効期限内であれば保存したaccess_tokenを利用することができます。

### 翻訳（translator.js）に投げ、結果を受け取ってユーザーへ送信する
「ユーザーのメッセージを受け取りメッセージに改ざんがないか確認する」にて、改ざんされていないことが確認できたら、「翻訳（translator.js)」にメッセージを投げて、翻訳結果を受け取ります。

```index.js
translate.replyTrans(message, (afterMessage) => {
    // 翻訳結果をユーザーへ送信する
    sendMessage(newtoken, accountId, message, afterMessage);
});

function sendMessage(token, accountId,  message, aftermessage) {
    const postdata = {
        url: 'https://apis.worksmobile.com/' + API_ID + '/message/sendMessage/v2',
        headers : {
          'Content-Type' : 'application/json;charset=UTF-8',
          'consumerKey' : CONSUMERKEY,
          'Authorization' : "Bearer " + token
        },
        json: {
            "botNo" : Number(BOTNO),
            "accountId" : accountId,
            "content" : {
                "type" : "text",
                "text" : message + "\n ------------------------------ \n" + aftermessage
            }
        }
    };
    request.post(postdata, (error, response, body) => {
        if (error) {
          console.log(error);
        }
        console.log(body);
    });
}
```

## 翻訳（translator.js)

いよいよ翻訳部分を実装していきます！

### AzureADの設定

①　[https://portal.azure.com/](https://portal.azure.com/)アクセスし、全体管理者でログイン。
②　画面上部の「リソース、サービス、ドキュメントの検索」欄で「Cognitive Services」を検索します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/df9864f0-5493-27f9-a941-e242716df079.png)

③[+追加]をクリックします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/57c961a3-bf24-3bca-8acd-5d0d9ce6d4b6.png)

④ [Translator Text API]を検索してクリックします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/ed3c766b-e4a6-f306-adca-1789bc732fcf.png)

⑤ [作成]をクリックします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/5915b1db-a0ce-07ce-4910-3a7b95faabd1.png)

⑥　[Name]には利用用途がわかるように記載しておくと良いと思います。それ以外の設定項目は環境に合わせて設定してください。完了したら、[作成]をクリックしてTranslator Text　APIの登録完了です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/738351c3-a049-f55b-d3b8-6fcec23513ec.png)


⑦　さて、このTranslator Text　APIをBotから叩けるようにします。ダッシュボードに先ほど登録したTranslator Text APIサービスが表示されていると思うので、クリックします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/26d63af7-5817-64f7-0403-f5f90369fa93.png)

⑧ 左メニューで[Keys]を選択し、[KEY1]をコピーします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/358e8bfc-ef4e-d441-a992-add64b8ab4fb.png)

⑨Botから呼び出せるように、　Herokuの環境変数に設定しておきます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/88cc30b4-f517-779f-47a8-49918295d867.png)


これで準備はバッチリです！
次はプログラミングしていきます。

### Bot本体(index.js)からのメッセージを受け取り翻訳し、Bot本体(index.js)へ返す

Translator Text APIを使うには下記のような流れで進めます。

1.AzureのKeyを使って、AccessTokenを取得
2.AccessTokenを使ってTranslator　Text APIを叩く。

それぞれ解説します。

#### 1.AzureのKeyを使って、AccessTokenを取得

```translator.js

exports.replyTrans = function (message, callback){
    //実行
    getAccessToken((token) => {
        //翻訳処理を書く
        ：
    });
}

//アクセストーク取得
function getAccessToken(callback) {
    let body = '';
    let data = '';

    let req = https.request({
        host: 'api.cognitive.microsoft.com',
        path: '/sts/v1.0/issueToken',
        method: 'POST',
        headers: {
          'Ocp-Apim-Subscription-Key': azureClientSecret
        }
    }, (res) => {
        res.setEncoding('utf8');
        res.on('data', (chunk) => {
            body += chunk;
        }).on('end', () => {
            console.log(body);
            let resData = body;
            callback(resData);
        });

    }).on('error', (err) => {
        console.log(err);
    });
    req.write(qs.stringify(data));
    req.end();
}

```
#### 2.AccessTokenを使ってTranslator　Text APIを叩く。

日本語、タイ語、英語の翻訳結果を受け取るよう要求リクエストを投げています。
細かいパラメーター等は変更可能ですので、[公式APIドキュメント](https://docs.microsoft.com/ja-jp/azure/cognitive-services/translator/reference/v3-0-translate?tabs=curl)を参照してください。

```translator.js
function translate(token, message, callback) {
    const postdata = {
        url: 'https://api.cognitive.microsofttranslator.com/translate?category=generalnn&api-version=3.0&to=ja&to=th&to=en&profanityAction=Marked&profanityMarker=Asterisk',
        headers : {
          'Content-Type' : 'application/json',
          'Authorization' : "Bearer " + token
        },
        body : JSON.stringify ([{'Text' : message}])
    };
    request.post(postdata, (error, response, body) => {
        console.log(postdata);
        if (error) {
          console.log(error);
        }
        console.log(body);
        callback(body);
    });
}
```

翻訳結果は、Translator Text APIからは翻訳対象の言語コード、翻訳後の言語コードと文章が返ってきます。
そのため、翻訳対象の言語が「日本語」の場合は、「タイ語」に翻訳したものを返します。
翻訳対象の言語が「タイ語」の場合は、「日本語」に翻訳したものを返します。
翻訳対象の言語が「日本語」でも「タイ語」でもない場合は、「英語」に翻訳したものを返します。

全体のサンプルコードはこのようにしました。

```translator.js
'use strict';

const http = require('http');
const https = require('https');
const qs = require('querystring');
const request = require("request");

const azureClientSecret = process.env.azureClientSecret;

//アクセストーク取得
function getAccessToken(callback) {
    let body = '';
    let data = '';

    let req = https.request({
        host: 'api.cognitive.microsoft.com',
        path: '/sts/v1.0/issueToken',
        method: 'POST',
        headers: {
          'Ocp-Apim-Subscription-Key': azureClientSecret
        }
    }, (res) => {
        res.setEncoding('utf8');
        res.on('data', (chunk) => {
            body += chunk;
        }).on('end', () => {
            console.log(body);
            let resData = body;
            callback(resData);
        });

    }).on('error', (err) => {
        console.log(err);
    });
    req.write(qs.stringify(data));
    req.end();
}

//翻訳
function translate(token, message, callback) {
    const postdata = {
        url: 'https://api.cognitive.microsofttranslator.com/translate?category=generalnn&api-version=3.0&to=ja&to=th&to=en&profanityAction=Marked&profanityMarker=Asterisk',
        headers : {
          'Content-Type' : 'application/json',
          'Authorization' : "Bearer " + token
        },
        body : JSON.stringify ([{'Text' : message}])
    };
    request.post(postdata, (error, response, body) => {
        console.log(postdata);
        if (error) {
          console.log(error);
        }
        console.log(body);
        callback(body);
    });
}

exports.replyTrans = function (message, callback){
    //実行
    getAccessToken((token) => {
        translate(token, message, (translated) => {
            let res = JSON.parse(translated);
            console.log(res[0]["detectedLanguage"]["language"]);
            if (res[0]["detectedLanguage"]["language"] == "ja"){
                //thを返す
                callback(res[0]["translations"][1]["text"]);
            } else if (res[0]["detectedLanguage"]["language"] == "th"){
                //jaを返す
                callback(res[0]["translations"][0]["text"]);
            } else {
                //enを返す
                callback(res[0]["translations"][2]["text"]);
            }
        });
    });
}

```

# 動作確認

さあ、色々な言語でBotに話しかけてみましょう！

日本語→タイ語
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/30771b48-81fc-dc67-adca-347b9e18aa00.png)

タイ語→日本語
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/16c04294-3a76-3c61-7a08-e7e0bf74ed94.png)

スペイン語→英語
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135983/7a428881-08e0-ee2d-7b3b-c1ad388b592d.png)

# 最後に
今回も長い記事になってしまいましたが、最後まで読んでいただきありがとうございます。
今回は、Translator Text APIとLINE WORKSを組み合わせたBotを紹介しました。このように色々なAPIとつなぐことでBotの能力は上がっていきます。
LINE WORKSをお使いの皆さま、ぜひご活用ください〜♪