やりたいことの概要
- SlackからRedmineにある期限が切れそうなチケットを取得する。
- Slackからは、Slash Commandsを使ってこれを実現する。
- Slash Commadnsが呼び出すAPIはAmazon API Gateway + AWS Lambdaで作成する。
- 取得したチケットのidがSlackに表示される。
要約すると、Redmine上で終わってないタスクのリマインドをSlack上のコマンド一発で確認できるようになりたい!という願望です。
全体像
slackのslash commandの設定
-
https://.slack.com/services/new にアクセスする
※この時slash commandを使いたいslackにログインしていることが前提 -
検索窓に「slash command」と入力して出てきたSlash Commandsをクリック
-
「Choose a Command」に使いたいスラッシュコマンドを入力、addする
-
無事に作成されたら、後で使うのでTokenの値をどこかに取っておく
※Slash Commandsのページから確認もできる -
Slash Commandsのページの「Settings」に移動し、作成した項目の右側のえんぴつマークを押す
-
「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の初回実行時は、タイムアウトになってしまうこともありました。
結びの言葉
チケットの期限は守りましょう!