SlackAPIを用いて便利なアップローダーbotを作りました。

はじめに

こんにちは。
サービシンク受託事業部でエンジニアをしています。熊谷です。

僕はbotを作るのが好きで今まで色々なbotを作ってきました。
その中でも社内で評判の良かった便利なbotがあります。

その名も「アップローダーbot」です。

今回はそのbotについて紹介しようと思います。

アップローダーbotについて

アップローダーbotとはなんですか?というところから始まりますよね。説明します。

簡単にいってしまうとSlackでファイルをアップロードするとダウンロードURLを返してくれるbotです。

これの何がいいかというと、例えばGmailで1ファイルの容量が大きすぎてファイルを添付できない時ってあったりしますよね。かといってGoogleドライブの容量を圧迫させたくない、そういったときにアップローダーbotがあればファイルをダウンロードURLに変換してメール本文に挿入できるので便利です。
また、アップロードされたファイルで1週間以上経過したファイルを削除するような定期プログラムを設定すればずっとダウンロードURLが生きつづけることもないので安全です。

仕組みは非常にシンプルです。
ファイル保管用のサーバーにスクリプトを設置しておき、Slack上でユーザーがファイルをアップロードしたら実行してSlackにアップロードされたファイルをダウンロード、ダウンロード用のURLを発行してSlackに通知しています。

図で説明すると↓の様な感じです。

アップローダーbotのソースコード

気になるのはプログラムですよね。

PHPで実装していて、プログラムのソースコードは以下です。

<?php
ini_set("error_log", __DIR__."/log/php_error.log");

// SlackAPIからのリクエストを取得しています。
$request = json_decode(file_get_contents('php://input'), true);

// SlackAPIからのリクエストかどうか検証しています。
if ($request['token'] !== 'xxxxxxxxxxxxxxxxxxxxxxxx') {
      error_log('不正なリクエスト: ip ' . $_SERVER['REMOTE_ADDR']);
    exit;
}

// SlackAPIからのテストリクエストを処理しています。
if ($request['type'] === 'url_verification') {
    json_encode($request, true);
      echo $request['challenge'];
      exit;
}

// Slackのリトライ対策としてヘッダーの「HTTP_X_SLACK_RETRY_NUM」をチェックする
// 理由は後述します。
if ($request['event']['files'] && !isset($_SERVER['HTTP_X_SLACK_RETRY_NUM'])) {
    $channel = $request['event']['channel'];
    $eventTs = $request['event']['ts'];
    foreach ($request['event']['files'] as $key => $file) {
        // アップロードの前にファイル名や保存形式のチェックを行っています。
        $err = validateFile($file);
        if ($err) {
            // エラーがある場合はエラーメッセージをSlackに通知
            postSlackMessage($channel, $err, $eventTs);
            exit;
        }
        // ダウンロード用のシェルスクリプトを実行しています。
        // 実際はOSインジェクション対策でエスケープしています。
        // 理由は後述します。
        shell_exec('/bin/sh ./getFile.sh "' . $file['url_private_download'] . '"');
        // ダウンロード用のURLを生成してSlackに通知しています。
        $downloadUrl = 'download/path/to/' . $file['name'];
        $slackParams = [
            'color' => "#7fff00",
            'pretext' => 'アップロードが完了しました。',
            'title' => 'ダウンロードURL',
            'text' => $downloadUrl,
        ];
        postSlackMessage($channel, $slackParams, $eventTs);
    }
}

/**
 * ファイル名は半角英数字
 * ファイル形式はzip形式
 *
 * @param array $file
 * @return void
 */
function validateFile($file)
{
    $err = '';

    if (!preg_match("/^[a-zA-Z0-9_.-]+$/", $file['name'])) {
        $err = [
            'color' => 'danger',
            'pretext' => 'ファイル名は「半角英数字」にしてください。',
            'title' => '対象ファイル名',
            'text' => $file['name'],
        ];
    } 

    if (pathinfo($file['name'], PATHINFO_EXTENSION) != 'zip') {
        $err = [
            'color'   => 'danger',
            'pretext' => 'ファイル形式は「zip」にしてください。',
            'title'   => '対象ファイル',
            'text'    => $file['name'],
        ];
    } 
    
    return $err;
}

/**
 * Slackにチャットを送る
 *
 * @param $channel
 * @param array $attachments
 * @param $ts
 */
function postSlackMessage($channel, $slackParams, $ts)
{
    $url = "https://slack.com/api/chat.postMessage";

    $header = [
        'Authorization: Bearer xoxb-1234567890123-123456789012345-xxxxxxxxxxxxxxxxxxxxxxxx',
        'Content-Type: application/json;charset=UTF-8',
    ];

    $post_field = [
        "channel" => $channel,
        "attachments" => $slackParams,
        "thread_ts" => $ts
    ];

    $options = [
        CURLOPT_URL => $url,
        CURLOPT_HTTPHEADER => $header,
        CURLOPT_CUSTOMREQUEST => 'POST',
        CURLOPT_POSTFIELDS => json_encode($post_field),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER => true,
    ];

    $ch = curl_init();
    curl_setopt_array($ch, $options);
    curl_exec($ch);
    curl_close($ch);
}

このプログラムには補足ポイントが2つあります。

1つ目は以下です。

if ($request['event']['files'] && !isset($_SERVER['HTTP_X_SLACK_RETRY_NUM'])) {}

SlackAPIはリクエストの返却に時間がかかるとリクエストをリトライします。

そのため、容量の大きいファイルをアップロードした時に処理に時間がかかってしまい、処理が重複して実行されてしまうことがありました。

上記はそれを回避するための条件です。SlackAPIがリトライのリクエストを送ると「HTTP_X_SLACK_RETRY_NUM」という名前のヘッダーが一緒に送られてきます。これを判定処理に使うことで重複を避けるようにしています。

2つ目は以下です。

shell_exec('/bin/sh ./getFile.sh "' . $file['url_private_download'] . '"');

なぜシェルスクリプト使ってるんだって思いますよね。僕も思います。

これは保管用のサーバーがレンタルサーバーだったことが大きな理由でした。

契約していたレンタルサーバーはPHPが使用できるメモリー上限を一定までしか上げることができませんでした。

そのため重たいファイルをSlackからダウンロードしようとすると、どうしてもメモリーリークを起こしてしまいうまくいきませんでした。そこでPHPで処理するのは難しいという判断に至り、上記のようなコードになりました。

ちなみに実行しているシェルスクリプトは以下です。

#!/bin/bash

TOKEN="xoxb-1234567890123-123456789012345-xxxxxxxxxxxxxxxxxxxxxxxx"

// $1$file['url_private_download']がきます。
cd /path/to/ && { curl -H "Authorization:Bearer $TOKEN" -sS -O $1 ; cd -; }

終わりに

以上で紹介は終わりです。

ここまで読んでいただきありがとうございました。

書いていて「これやってることアップローダーというよりダウンローダーだな」と思いました。入社1年目の時に考えてから3年が経ってしまっているのでまぁいいでしょう。

会社でbot作ってみたいけど何作ればいいかわからないなーと悩んでいる方や、なんかいい感じのbotアイデアないかなーと探している方も参考にしていただけたら幸いです。

最近はクラウドサーバーが主流なので、アップローダーbotもAWSなんかに移行してついでにPHPではなくGoで実装したいなーと思っているので実施したらまた記事を書こうと思います。

それではまたどこかで。

Webサイト・システムの
お悩みがある方は
お気軽にご相談ください

お問い合わせ 03-6380-6022(平日09:30~18:30)

出張またはWeb会議にて、貴社Webサイトの改善すべき点や
ご相談事項に無料で回答いたします。

無料相談・サイト診断 を詳しく見る

多くのお客様が気になる情報をまとめました、
こちらもご覧ください。