Slackにアップしたファイルを自動でS3にアップするBOTを作ってみた -ファイルはS3にどんどんしまっちゃおうねぇ～-

みんな大好き[Slack](https://slack.com)ですが、無料プランのままファイルをアップしていくとどんどん古いのが消されるのが悩みどころ。
そこでSlackにファイルがアップされたら自動でS3に移すBOT(しまっちゃうおじさんと命名)を作りました。
~~おとなしく有料プランにすればいいんじゃないすかねぇ...~~

[※しまっちゃうおじさんとは](http://dic.nicovideo.jp/a/しまっちゃうおじさん)

*2016/09/30*
githubにソースをアップしました。

- [Mic-U/putting_uncle](https://github.com/Mic-U/putting_uncle)

# 環境構築

一からBOTを作るのは辛いので[Botkit](https://github.com/howdyai/botkit)というフレームワークを使います。
また、SlackのAPIにアクセスするためのトークンをあらかじめ発行しておく必要があります。

その辺は↓の記事が非常にわかりやすく解説してくれています。

- [Slack の BotKit を使う : まだプログラマーですが何か？](http://dotnsf.blog.jp/archives/1054134334.html)

# 実装

## controller追加

Botkitをインストールすると色々ファイルが出来上がりますが、今回いじるのはslack_bot.jsです。([githubのソース](https://github.com/howdyai/botkit/blob/master/slack_bot.js))

デフォルトですでに何個かハンドラーがありますが、ファイルアップロード時のハンドラーを追加します。
[公式のドキュメント](https://api.slack.com/rtm)によるとfile_sharedというイベントがあるから、これをハンドラーに登録すればいいんだな!

```js:slack_bot.js
controller.on('file_shared', function(bot, message){
    //TODO ファイルを受け取る
    //TODO ファイルをS3に上げる
});
```

実はここがトラップです。
実際にはBotkitはmessageイベントの中のサブイベントというのを見てコントローラーに振り分けたりしてるみたいなので、[ここ](https://api.slack.com/events/message)を参照してイベント名をつけてやります。
よく見たら**file_share**となってますね。

file_sharedでもBotkitがよしなにやってくれるのでファイルをS3にアップすることはできるのですが、どのチャンネルでファイルがアップされたのかとかがわからなくなって後々困ります。

というわけで

```js:slack_bot.js
controller.on('file_share', function(bot, message){ //sharedじゃなくてshare
    //TODO ファイルを受け取る
    //TODO ファイルをS3に上げる
});
```

## コントローラーの中身を実装する、その前に...

さて、これからコントローラーの中身を実装するわけですが、しまっちゃうおじさんが何をするべきか整理しましょう。

1. メッセージからファイルが保存されているURLを取得する
2. 1.で取得したURLにアクセスしてファイルの中身をゲットする
3. 2.でゲットしたファイルの中身をS3にアップする(バケット/チャンネル名/日付みたいにフォルダ分けしたい)
4. S3にアップした旨をSlackに返信

コールバック地獄のにおいがぷんぷんしますね。
というわけで、先日からマイブームの[co](https://github.com/tj/co)兄貴の力を借りつつ進めます。

## ファイルのurlを取得する

ファイルのメタ情報に関しては、ハンドラーメソッドの引数であるmessageに含まれてます。
詳しくは[こちら](https://api.slack.com/events/message/file_share)

message.fileのurl_private_downloadにダウンロード用のURLが保存されています。

```js:slack_bot.js
controller.on('file_share', function(bot, message){ //sharedじゃなくてshare
    const fileURL = message.file.url_private_download;
    //TODO ファイルを受け取る
    //TODO ファイルをS3に上げる
});
```

privateとついてる辺りに嫌な予感がしますね。

## ファイルのコンテンツを取得する
さて、ファイルのURLはわかったのでコンテンツを取得します。
単純にリクエストを送りたいところですが、そのままだと認証ではじかれてしまうのでヘッダーにトークンを仕込んでやります。

coを使っていきたいので、ファイルのコンテンツ取得はメソッドを分けましょう

```js
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](https://api.slack.com/methods/channels.info)、プライベートなら[groups.info](https://api.slack.com/methods/groups.info)を使います。
どちらを使うべきか見分けるポイントはチャンネルIDの頭文字がCかGかです。

というわけでこんな感じ

```js
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を指定してやるとはかどります。

```js
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);
            }
        });
    });
}
```

## 完成図

以上を組み合わせるとコントローラはこんな感じになります。

```js:slack_bot.js
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にファイルを上げると...

![uncle.JPG](https://qiita-image-store.s3.amazonaws.com/0/116434/9be47b43-52f4-11e8-781d-979bb8ddc056.jpeg)

おじさんが目覚めてS3にファイルをしまってくれます!

SlackのAPIは罠が多い + 情報が少ないので大変でした。
Slack BOTを作りたいという方の助けになれば幸いです。