SlackでRedmineの情報をとってくる

#やりたいことの概要
- SlackからRedmineにある期限が切れそうなチケットを取得する。
- Slackからは、Slash Commandsを使ってこれを実現する。
- Slash Commadnsが呼び出すAPIはAmazon API Gateway + AWS Lambdaで作成する。
- 取得したチケットのidがSlackに表示される。

要約すると、Redmine上で終わってないタスクのリマインドをSlack上のコマンド一発で確認できるようになりたい！という願望です。

#全体像
![構成図.PNG](https://qiita-image-store.s3.amazonaws.com/0/153290/070e4fb1-1319-3fc0-82d9-3f989c4e1554.png)



#slackのslash commandの設定
1. https://.slack.com/services/new にアクセスする
　※この時slash commandを使いたいslackにログインしていることが前提

2. 検索窓に「slash command」と入力して出てきたSlash Commandsをクリック

3. 以下の画面が表示されたら、「Add Configuration」をクリック
![image.png](https://qiita-image-store.s3.amazonaws.com/0/153290/b76195c6-326f-4be3-16d9-7049fb850572.png)

4. 「Choose a Command」に使いたいスラッシュコマンドを入力、addする

5. 無事に作成されたら、後で使うのでTokenの値をどこかに取っておく
※Slash Commandsのページから確認もできる

6. Slash Commandsのページの「Settings」に移動し、作成した項目の右側のえんぴつマークを押す

7. 「URL」の欄にAPI GateWayのエンドポイントを入力する

#lamdbaの処理部分
## slackとの連携
lambdaで用意されている、BluePrint「Slack-echo-command」を使用。
今回は、node.js 6.10 を使用しました。
lambdaの環境変数に、kmsEncryptedToken（BluePrint上でデフォルトで入っている変数名）＝ 上で発行したToken　を指定する。

下記のドキュメントを参考にさせていただきました。
http://dev.classmethod.jp/cloud/aws/slack-integration-blueprint-for-aws-lambda/

## redmineのAPI呼び出し

公式のドキュメントを参考にAPI呼び出す部分を作る。
http://redmine.jp/glossary/r/rest-api/
http://www.redmine.org/projects/redmine/wiki/Rest_api
今回はNode.jsを利用して呼ぶようなソースを作るので、こちらを参考にさせていただいた。
http://blog.honjala.net/entry/blog/2015/07/13/post-580/

### 書いたもの
**get_ticket.js**
プロジェクト名とか期日とかパラメータをして該当のチケットを持ってくるやつ。
requireしてるものは適当にpackage.json当たりに入れておいてください。

```javascript
var RestClient = require('node-rest-client').Client;
var util = require('util');

var ENDPOINT_BASE = 'redmineのルートURL';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

var rmClient = new RestClient();

//チケットの読み取り
exports.getTicketList = function (queries, params, project, callback) {
    //API上のパラメータ
    var limit = queries.limit ? queries.limit:100;
    var sort = queries.sort ? queries.sort:'due_date';
    var offset = queries.offset ? queries.offset:'0';
    //Redmineは1リクエストで100件が上限なので必要があれば呼び出し先でループすること
    rmClient.get(ENDPOINT_BASE + '/projects/' + (project ? project:'redmine_sandbox') + '/issues.json?limit='+ limit +'&sort=' + sort +'&offset=' + offset, {
        headers: {
            'Content-type': 'application/json',
            'X-Redmine-API-Key': 'Redmineで発行したAPIキーを入れる'
        },
        parameters: params
    }, function (data, response) {
        // var result = JSON.parse(response.toString('utf8'));
        callback(data);
    });
};

//締め日を考えてとってくるときの日付比較　moment.js使うといいよと書いてから教わる
exports.compare2now = function compare2now( datestr )
{
    // 現在の日付＆時刻を設定　LambdaだとUTCになるかも
    var today = new Date();
    today.setHours(0);
    today.setMinutes(0);
    today.setSeconds(0);
    today.setMilliseconds(0);
    
    // チケットの文字列から年月日を抜き出し、数値型に変換
    var vYear = parseInt( datestr.substr( 0, 4  ),10);
    var vMonth = parseInt( datestr.substr( 5, 2 ),10 ) -1;
    var vDay = parseInt( datestr.substr( 8, 2 ),10 );
    var adate = new Date( vYear, vMonth, vDay );

    if( adate.getTime() <= today.getTime() ){
        return 1;
    }else{
        return -1;
    }
}
```

**index.js**
Lambdaで指定する方のファイル

```javascript
'use strict';
const AWS = require('aws-sdk');
const qs = require('querystring');
const rm = require('./get_tickets.js');

const kmsEncryptedToken = process.env.kmsEncryptedToken;
let token;


function processEvent(event, callback) {
    const params = qs.parse(event.body);
    const requestToken = params.token;
    if (requestToken !== token) {
        console.error(`Request token (${requestToken}) does not match expected`);
        return callback('Invalid request token');
    }

    // slash command parameter
    const user = params.user_name;
    const command = params.command;
    const channel = params.channel_name;

    // redmine api parameter
    const project = 'redmine_sandbox'; //project name
    const limit = 100; // limit of query(max 100)
    const offset = '0'; // offset (use for paging)
    
    const commandText = [];
    commandText.push('まだ終わってないチケットだよ');
    rm.getTicketList({limit:limit,sort:'due_date',offset:offset},{}, project, function (data) {
        for(let issue of data.issues) {
            if(issue.due_date && rm.compare2now(issue.due_date)>0 ){
                if(issue.project && issue.project.name){
                    commandText.push(`project:${issue.project.name}`);
                }
                if(issue.id){ 
                    commandText.push(`, id:${issue.id}`);
                }
                if(issue.assigned_to && issue.assigned_to.name){
                    commandText.push(`, assigned_to:${issue.assigned_to.name}`);
                }
            }
        }
        //とってきた結果をslackに返す
        callback(null, `${user} invoked ${command} in ${channel} with the following text: ${commandText.join('\n')}`);
    }); 
}


exports.handler = (event, context, callback) => {
    const done = (err, res) => callback(null, {
        statusCode: err ? '400' : '200',
        body: err ? (err.message || err) : JSON.stringify(res),
        headers: {
            'Content-Type': 'application/json',
        },
    });

    if (token) {
        // Container reuse, simply process the event with the key in memory
        processEvent(event, done);
    } else if (kmsEncryptedToken && kmsEncryptedToken !== '<kmsEncryptedToken>') {
        const cipherText = { CiphertextBlob: new Buffer(kmsEncryptedToken, 'base64') };
        const kms = new AWS.KMS();
        kms.decrypt(cipherText, (err, data) => {
            if (err) {
                console.log('Decrypt error:', err);
                return done(err);
            }
            token = data.Plaintext.toString('ascii');
            processEvent(event, done);
        });
    } else {
        done('Token has not been set.');
    }
};

```
出来上がったらZipで固めてLambdaに上げる。

#やってみた結果
Slackから、最初に作ったslash commandを実行します。
　/[スラッシュコマンド名]　を実行（後ろに引数も入れられますが、今回は使用していません。）
⇒ index.js に書いた、callbackの中身がSlackに表示されます！

- LambdaのBluePrint「Slack-echo-command」を使用すると、Slackで発行したtokenをLambdaの環境変数に入れるだけで動かせてチョー簡単。
- Redmineへの通信経路にIP制限が設定されていたため、Lambdaからのリクエスト時のIPアドレスを固定するために、LambdaをVPC内に作成し、リクエストがNAT Gatewayから出ていくようにしました。
- slash commanddでは、レスポンスが3sでタイムアウトになるため、Lambdaの初回実行時は、タイムアウトになってしまうこともありました。

#結びの言葉
チケットの期限は守りましょう！