LoginSignup
6
5

More than 5 years have passed since last update.

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

Posted at

やりたいことの概要

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

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

全体像

構成図.PNG

slackのslash commandの設定

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

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

  3. 以下の画面が表示されたら、「Add Configuration」をクリック
    image.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当たりに入れておいてください。

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で指定する方のファイル

'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の初回実行時は、タイムアウトになってしまうこともありました。

結びの言葉

チケットの期限は守りましょう!

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5