みんな大好きSlackですが、無料プランのままファイルをアップしていくとどんどん古いのが消されるのが悩みどころ。
そこでSlackにファイルがアップされたら自動でS3に移すBOT(しまっちゃうおじさんと命名)を作りました。
おとなしく有料プランにすればいいんじゃないすかねぇ...
2016/09/30
githubにソースをアップしました。
環境構築
一からBOTを作るのは辛いのでBotkitというフレームワークを使います。
また、SlackのAPIにアクセスするためのトークンをあらかじめ発行しておく必要があります。
その辺は↓の記事が非常にわかりやすく解説してくれています。
実装
controller追加
Botkitをインストールすると色々ファイルが出来上がりますが、今回いじるのはslack_bot.jsです。(githubのソース)
デフォルトですでに何個かハンドラーがありますが、ファイルアップロード時のハンドラーを追加します。
公式のドキュメントによるとfile_sharedというイベントがあるから、これをハンドラーに登録すればいいんだな!
controller.on('file_shared', function(bot, message){
//TODO ファイルを受け取る
//TODO ファイルをS3に上げる
});
実はここがトラップです。
実際にはBotkitはmessageイベントの中のサブイベントというのを見てコントローラーに振り分けたりしてるみたいなので、ここを参照してイベント名をつけてやります。
よく見たらfile_shareとなってますね。
file_sharedでもBotkitがよしなにやってくれるのでファイルをS3にアップすることはできるのですが、どのチャンネルでファイルがアップされたのかとかがわからなくなって後々困ります。
というわけで
controller.on('file_share', function(bot, message){ //sharedじゃなくてshare
//TODO ファイルを受け取る
//TODO ファイルをS3に上げる
});
コントローラーの中身を実装する、その前に...
さて、これからコントローラーの中身を実装するわけですが、しまっちゃうおじさんが何をするべきか整理しましょう。
- メッセージからファイルが保存されているURLを取得する
- 1.で取得したURLにアクセスしてファイルの中身をゲットする
- 2.でゲットしたファイルの中身をS3にアップする(バケット/チャンネル名/日付みたいにフォルダ分けしたい)
- S3にアップした旨をSlackに返信
コールバック地獄のにおいがぷんぷんしますね。
というわけで、先日からマイブームのco兄貴の力を借りつつ進めます。
ファイルのurlを取得する
ファイルのメタ情報に関しては、ハンドラーメソッドの引数であるmessageに含まれてます。
詳しくはこちら
message.fileのurl_private_downloadにダウンロード用のURLが保存されています。
controller.on('file_share', function(bot, message){ //sharedじゃなくてshare
const fileURL = message.file.url_private_download;
//TODO ファイルを受け取る
//TODO ファイルをS3に上げる
});
privateとついてる辺りに嫌な予感がしますね。
ファイルのコンテンツを取得する
さて、ファイルのURLはわかったのでコンテンツを取得します。
単純にリクエストを送りたいところですが、そのままだと認証ではじかれてしまうのでヘッダーにトークンを仕込んでやります。
coを使っていきたいので、ファイルのコンテンツ取得はメソッドを分けましょう
function download(url, token){
return new Promise(function(resolve, reject){
request({method: 'get',
url:url,
encoding: null,
headers: {Authorization: "Bearer " + token} //ヘッダーにトークンを仕込まないとダメ
},function(error, response, body){
if(error){
reject(error);
}else{
resolve(body);
}
})
});
}
ファイルをS3にしまっちゃう
チャンネル名を取得する
チャンネルの情報もmessageに入ってます、と言いたいところですがmessageに含まれているのはC1234567890みたいな形のチャンネルIDだけです。
さすがにこれでフォルダ分けしてもわからないので、チャンネル名をAPIで取得します。
そしてここでもSlack APIのトラップなのですが、対象がパブリックチャンネルかプライベートチャンネルで別のAPIを使う必要があります。
えぇ...
パブリックならchannels.info、プライベートならgroups.infoを使います。
どちらを使うべきか見分けるポイントはチャンネルIDの頭文字がCかGかです。
というわけでこんな感じ
function getChannelName(token, channelID) {
let apiURL = 'https://slack.com/api/';
let attribute;
if(channelID.charAt(0) === "C"){
apiURL += "channels.info?";
attribute = "channel";
}else if(channelID.charAt(0) === "G"){
apiURL += "groups.info?";
attribute = "group";
}else{
return "undefined";
}
apiURL += ('token=' + token);
apiURL += ('&channel=' + channelID);
return new Promise(function(resolve, reject){
request({method: 'get',
url:apiURL,
encoding: null
}, function(error, response, body){
if(!error && response.statusCode === 200){
let result = JSON.parse(body.toString());
resolve(result[attribute].name);
}else{
reject(error);
}
})
});
}
ファイルをS3にアップする
さて、S3へのアップもサクッと済ませましょう。
ContentTypeを指定してやるとはかどります。
function uploadToS3(content, fileInfo, channelName){
const AWS = require('aws-sdk');
AWS.config.update({
accessKeyId: 'access-key',
secretAccessKey: 'secret-key',
region: 'region'
});
const BUCKET = 'bucket-name';
const s3 = new AWS.S3({params: {Bucket: BUCKET}});
const moment = require('moment');
return new Promise(function(resolve, reject){
s3.upload({
Key: channelName + '/' + moment().format('YYYYMMDD') + '/' + fileInfo.name,
Body: content,
ContentType: fileInfo.mimetype
},function(error, data){
if(error){
reject(error);
}else{
resolve(data);
}
});
});
}
完成図
以上を組み合わせるとコントローラはこんな感じになります。
controller.on('file_share', function(bot, message){
'use strict';
const co = require('co');
const request = require('request');
const channelID = message.channel;
co(function * (){
try{
const channelName = yield getChannelName(token, channelID);
const fileInfo = message.file;
const content = yield download(fileInfo.url_private_download, token);
yield uploadToS3(content, fileInfo, channelName);
}catch(e){
throw new Error(e);
}
}).then(function(){
bot.reply(message, 'さぁ~どんどんしまっちゃおうねぇ~'); //成功時の返信
}).catch(function(e){
console.log(e);
bot.reply(message, 'すまんな'); //失敗時の返信
});
function getChannelName(token, channelID) {
var apiURL = 'https://slack.com/api/';
var attribute;
if(channelID.charAt(0) === "C"){
apiURL += "channels.info?";
attribute = "channel";
}else if(channelID.charAt(0) === "G"){
apiURL += "groups.info?";
attribute = "group";
}else{
return "undefined";
}
apiURL += ('token=' + token);
apiURL += ('&channel=' + channelID);
return new Promise(function(resolve, reject){
request({method: 'get',
url:apiURL,
encoding: null
}, function(error, response, body){
if(!error && response.statusCode === 200){
let result = JSON.parse(body.toString());
resolve(result[attribute].name);
}else{
reject(error);
}
})
});
}
function download(url, token){
return new Promise(function(resolve, reject){
request({method: 'get',
url:url,
encoding: null,
headers: {Authorization: "Bearer " + token}
},function(error, response, body){
if(error){
reject(error);
}else{
resolve(body);
}
})
});
}
function uploadToS3(content, fileInfo, channelName){
const AWS = require('aws-sdk');
AWS.config.update({
accessKeyId: 'access-key',
secretAccessKey: 'secret-key',
region: 'region'
});
const BUCKET = 'bucket-name';
const s3 = new AWS.S3({params: {Bucket: BUCKET}});
const moment = require('moment');
return new Promise(function(resolve, reject){
s3.upload({
Key: channelName + '/' + moment().format('YYYYMMDD') + '/' + fileInfo.name, //チャンネル名/日付でフォルダ分け
Body: content,
ContentType: fileInfo.mimetype
},function(error, data){
if(error){
reject(error);
}else{
resolve(data);
}
});
});
}
});
こいつを起動した状態でSlackにファイルを上げると...
おじさんが目覚めてS3にファイルをしまってくれます!
SlackのAPIは罠が多い + 情報が少ないので大変でした。
Slack BOTを作りたいという方の助けになれば幸いです。