Dropboxのファイル追加をSlackに通知するフォルダ監視くんを作った

# はじめに
大学の研究室ではDrobpoxのフォルダを共有して、卒論や修論のファイルを置いてもらって、順番に添削を行っています。
添削後は添削済み用のフォルダに入れます。
フォルダ構成は以下のような感じです。
<img width="814" alt="添削フォルダ.png" src="https://qiita-image-store.s3.amazonaws.com/0/205024/d295bb01-6f00-3552-c634-d690085e6875.png">
原稿を添削したら、添削済みのフォルダに入れてSlackで通知します。
<img width="451" alt="reply.png" src="https://qiita-image-store.s3.amazonaws.com/0/205024/7be145fc-3d53-5cee-162b-1001e469f54f.png">
研究室では30名近くを1名で見ていて、
添削ごとに返信するのは正直手間です。
ですので、添削済みのフォルダに入れれば自動的に通知するアプリを作りました。

## 対象者
* Dropboxの変更をSlackで通知したい方

# 処理の流れ
1. Dropboxで監視したいフォルダの内容変更
1. Dropbox APIで変更が検知され、WebhookによりPOSTパラメータが送られる
=>Webhookについては[こちらの記事](https://qiita.com/soarflat/items/ed970f6dc59b2ab76169)を参考にしてください
1. あらかじめ指定されたWebサーバでPOSTパラメータを受け取る
1. SlackにPOSTパラメータを送る
1. Slackでメッセージが通知される

# 前準備
Dropboxアプリの登録 => [こちらの記事](https://qiita.com/tksnino/items/c39b509494cc7f05954a#phpでdropboxapiを使う)を参考にしてください

# Dropboxのアプリの作成
Dropboxのアプリを作成したら、アクセストークンを取得してメモしておきます。
そのあと、[files/list_folder](https://dropbox.github.io/dropbox-api-v2-explorer/#files_list_folder)でアクセストークンと監視したいフォルダのパスを入力します。
* フォルダのパスの例: ``/Lab/2_添削フォルダ_開発版/``
![list_folder.png](https://qiita-image-store.s3.amazonaws.com/0/205024/2b58b351-b7ed-865b-fd8a-6083b36b7ac2.png)
Submit Callを押すとResponseのところに、該当のフォルダ内容が表示されます。
![Response.png](https://qiita-image-store.s3.amazonaws.com/0/205024/1052fdcb-a275-75d7-148d-42bf199ef5bd.png)
上記の内容は、フォルダ「/Lab/2_添削フォルダ_開発版」に 「a.txt」 が入っている状況です。

このResponseの中にcursorがあります。これは(おそらく)フォルダの内容を時間軸で保持している状態になります。**このcursorの内容もメモしておきます。**

## Dropbox API用のプログラムの作成
Dropboxのフォルダで変更があったとき、通知を受け取るプログラムを作ります。
[こちらの記事 (ぱふぅ家のホームページ)](https://www.pahoo.org/e-soul/webtech/php06/php06-64-01.shtm)を参考にPHPブログラムを作っていきます。
これからのプログラムはWebサーバに置きます。ただし、**https**が使用されるWebサーバに限ります。
まずDropboxのアクセストークンをメモしたdropboxAccessToken.txtを作成します。

```dropboxAccessToken.txt
xxxxxxx <= Dropboxのアクセストークンを記述する
```

つぎに、先ほどメモしたcursorを記述します。

```cusor.txt
xxxxxxx <= cursorを記述する
```

DropboxからWebhookで通知を受け取ったら、
変更ファイルのみを抽出するプログラムを記載します。

```dropboxUpdateNotificationAPI.php
<?php

//Dropboxの更新を通知するクラス
class dropboxUpdateNotificationAPI {
	private $error;		//エラーフラグ（エラーがあればTRUE）
	private $errmsg;		//エラーメッセージ（APIが返す error_summary の内容）
	private $responses;	//直前の結果
	private $access_token_path; //Dropboxで得られるアクセストークンのファイルパス
	private $cursor_file_path; // cursorが保存されるファイルのパス

/**
 * コンストラクタ
 * @param	なし
 * @return	なし
*/
function __construct() {

	//$this->webapi = '';
	$this->error  = FALSE;
	$this->errmsg  = '';
	$this->responses = NULL;
	$this->access_token_path = __DIR__ . "/dropboxAccessToken.txt";
	$this->cursor_file_path = __DIR__ . "/cursor.txt";
}

/**
 * デストラクタ
 * @return	なし
*/
function __destruct() {
	unset($this->responses);
}
/**
 * Dropbox API v2：アクセストークンを用いたリクエスト
 * @param	string $url    リクエストURL
 * @param	array  $argv   Dropbox-API-Argに渡すパラメータ配列（省略可能）
 * @param	array  $post   POSTに渡すパラメータ配列（省略可能）
 * @param	string $upload アップロードするデータ
 * @return	bool TRUE：リクエスト成功／FALSE：失敗
*/
function requestDropboxAPIv2($url, $argv=NULL, $post=NULL, $upload=NULL) {
	$flagArg = FALSE;
	$headers[] = 'Authorization: Bearer ' . $this->getAccessToken();
	// cURLを使ってリクエスト
	$curl = curl_init() ;
	curl_setopt($curl, CURLOPT_URL , $url);
	curl_setopt($curl, CURLOPT_HEADER, 1) ;
	curl_setopt($curl, CURLOPT_SSL_VERIFYPEER , FALSE);
	curl_setopt($curl, CURLOPT_RETURNTRANSFER,  TRUE);
	curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
	curl_setopt($curl, CURLOPT_TIMEOUT, 5);
	if ($argv != NULL) {
		$headers[] = 'Dropbox-API-Arg: ' . json_encode($argv);
		$flagArg = TRUE;
	}
	if ($upload != NULL) {
		$headers[] = 'Content-Type: application/octet-stream';
		curl_setopt($curl, CURLOPT_POSTFIELDS, $upload);
		$flagArg = FALSE;
	} else if ($post != NULL) {
		$headers[] = 'Content-Type: application/json';
		curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($post));
	}
	curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);

	$res1 = curl_exec($curl);
	$res2 = curl_getinfo($curl);
	curl_close($curl);
	$res = substr($res1, $res2['header_size']);

	//結果処理
	//$this->webapi = $url;
	preg_match("/HTTP\/[0-9\.]+\s([0-9]+)\s(.+)/ims", $res1, $arr);
	if (! isset($arr[2])) {
		$this->error   = TRUE;
		$this->errmsg  = 'Dropbox API failure';
	} else if ($arr[1] >= 300) {
		$json = json_decode($res);
		$this->error   = TRUE;
		$this->errmsg  = $arr[2];
	} else {
		$this->responses = $flagArg ? $res : json_decode($res);
	}
	return (! $this->error);
}

/**
 * Dropbox API v2：更新された添削ファイル名の取得
 * @param	string $cursor:	カーソル
 * @return	ファイル名/FALSE: 存在しない
 * カーソルをfileに保存しておく
 */
function getUpdatedCorrection() {
	$url = 'https://api.dropboxapi.com/2/files/list_folder/continue';	//リクエストURL
	$post['cursor'] = $this->getCursor(); //カーソルの取得
	$res = array();

	if ($this->requestDropboxAPIv2($url, NULL, $post) == FALSE) {
		$res = FALSE;
	} else if (isset($this->responses->entries[0]->name)) { //ファイルが存在したら
		$res = $this->responses->entries; //配列にする
		//ファイルが存在するのでcursorの更新
		$this->saveCursor ($this->responses->cursor);
	}
	return $res;
}

/**
 *  Dropboxのアクセストークンを取得する関数
 * @return string	Dropboxのアクセストークン
 */
function getAccessToken() {
	$handle = fopen($this->access_token_path, "r") or //ファイルオープン
	$this->fopen_error_func($this->access_token_path);  //ファイルオープン失敗時に呼ばれる関数
	$at = fgets($handle);
	fclose($handle);
	return ($at);
}

/**
 * cursorの取得する関数
 * @return	string	cursor
*/
function getCursor() {
	$handle = fopen($this->cursor_file_path, "r") or //ファイルオープン
    			$this->fopen_error_func($this->cursor_file_path);  //ファイルオープン失敗時に呼ばれる関数
	$cursor = fgets($handle);
	fclose($handle);
	return ($cursor);
}

/**
 * cursorを保存する関数
 * @param	string	cursor
 * @return	なし
 */
function saveCursor($cursor) {
	$handle = fopen($this->cursor_file_path, "w"); //ファイルオープン
	fputs($handle, $cursor); //保存
    fclose($handle);
}

/**
 * ファイルオープンが失敗した時に呼ばれる関数
 * @param	string	$fname	ファイル名
 */
function fopen_error_func($fname) {
	$error_file = fopen ("error.log", "a");
    fputs($error_file, $fname . " was not opened.\n");
	fclose($error_file);
	die;
}
}
```

WebhookでPOSTパラメータを受け取るプログラムを作成します。

```webhook.php
<?php
//verification request
if(isset($_GET['challenge'])){
    echo htmlspecialchars($_GET['challenge']);
    die;
}

//Dropbox APIの読み込み
require_once(__DIR__ . '/dropboxUpdateNotificationAPI.php');
$pdb = new dropboxUpdateNotificationAPI();	//更新を通知するクラス

//更新ファイルを取得できない場合
if (($update_files = $pdb->getUpdatedCorrection()) == FALSE) {
    die;
}

$path = __DIR__ . "/dropbox.log"; //更新情報の記載
$file = fopen ($path, "w");
fputs($file, "---更新---\n");

foreach($update_files as $uf) {
    $uf_arr = (array) $uf;

    if($uf_arr[".tag"]=="file") {
        //文字列が含まれていない場合
        if (strpos($uf_arr["name"],'添削済み') === false) {
            continue;
        }
        fputs($file, $uf_arr["name"] . " が添削されました\n");
    }else if ($uf_arr[".tag"]=="deleted") {
        fputs($file, $uf_arr["name"] . "が 削除されました\n");
    }
}
fputs($file, "---更新ここまで---\n");
fclose ($file); //ファイルを閉じる
?>
```

冒頭の```$_GET['challenge']```は、DropboxのWebhookに登録すると、確認のために送られるので、その対応のために記載されます。
その後、Dropbox APIのアプリページでWebhookのアドレスを登録します。
![webhook.png](https://qiita-image-store.s3.amazonaws.com/0/205024/8052517a-127f-1dd6-47cd-0595de6134df.png)
ここのWebページは**https**である必要があります。
登録ができれば、該当のフォルダに何かファイルを入れてください。
うまくいけば```dropbox.log```が追加され、変更内容が記載されます。

#SlackのAppの生成

Slack側でapiを作成するために、以下のURLをアクセスします。
https://slack.com/services/new/incoming-webhook
「Post to Channel」でチャンネルを選択します。
Workspaceが異なる場合は、右上のアイコンで選択し直します。

いきなりチャンネルに通知させるのは他人に迷惑がかかるかもしれませんので、
``Privately to @xxx(you)``のように自分に送るといいかもしれません。

Incoming WebHooksのページのWebhookで「Webhook URL」をメモします。
![slack_webhook.png](https://qiita-image-store.s3.amazonaws.com/0/205024/b3d17d13-b047-1d0b-986b-3dd02cb38d8c.png)

```slackWebhookURL.txt
https://hooks.slack.com/services/xxxxxxx <= slackのWebhookのURLを記述する
```

Slackにメッセージを通知するプログラムです。

```slackMessageSendingAPI.php
<?php

//Slackにメッセージを通知するクラス
class slackMessageSendingAPI {
    private $webhookURL_path; //SlackのWebhook URLのファイルパス
    private $webhookURL;

/**
 * コンストラクタ
 * @pram    なし
 * @return  なし
 */
function __construct() {
    $this->webhookURL_path = __DIR__ . "/slackWebhookURL.txt";
    $this->webhookURL = $this->getWebhookURL(); //Webhook URLの取得
}

/**
 * webhook URLの取得
 * @param   なし
 * @return  string  Webhook URL
 */
private function getWebhookURL() {
    $handle = fopen($this->webhookURL_path, "r") or //ファイルオープン
    			$this->fopen_error_func($this->webhookURL_path);  //ファイルオープン失敗時に呼ばれる関数
	$webhookURL = fgets($handle);
    fclose($handle);
    return($webhookURL);
}

/**
 * ファイルオープンが失敗した時に呼ばれる関数
 * @param	string	$fname	ファイル名
 */
private function fopen_error_func($fname) {
	$error_file = fopen ("error.log", "a");
    fputs($error_file, $fname . " was not opened.\n");
	fclose($error_file);
	die;
}

/**
 * slackにメッセージ送信
 * @param   string  $message メッセージ
 * @return  なし
*/
public function sendMessage($message) {
    $message_data["text"] = $message;
    //メッセージのjson化
    $message_json = json_encode($message_data);
    //payloadの値としてURLエンコード
    $message_post = "payload=".urlencode($message_json);

    $slack_file = fopen ("slack.log", "w");
    fputs($slack_file, "message: " . $message . "\n");
    fputs($slack_file, "url: " . $this->webhookURL . "\n");
    fclose($slack_file);

    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL , $this->webhookURL);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_POST, true);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $message_post);
    curl_exec($curl);
    curl_close($curl);
}

// End of Class ===========================================================
}

?>
```

webhook.phpにSlack API用の内容を記述します。
すでに記載したwebhook.phpを編集します。

```webhooks.php
(略)

//Slack APIの読み込み
require_once(__DIR__ . '/slackMessageSendingAPI.php');
$psk = new slackMessageSendingAPI(); //slackにメッセージを送るクラス

(略)

foreach($update_files as $uf) {
    $uf_arr = (array) $uf;

    if($uf_arr[".tag"]=="file") {
        //文字列が含まれていない場合
        if (strpos($uf_arr["name"],'澤野添削済み') === false) {
            continue;
        }
        //slackメッセージ
        $psk->sendMessage($uf_arr["name"] . " が添削されました");
        fputs($file, $uf_arr["name"] . " が添削されました\n");
    }else if ($uf_arr[".tag"]=="deleted") {
        fputs($file, $uf_arr["name"] . "が 削除されました\n");
    }
}
(略)
```

これで、Dropboxの監視フォルダが変更されたら、
Slackで通知されます。

#おわりに
はじめてのQiitaに投稿しました。
不備などあるかもしれませんが、
ご連絡いただけると助かります。

#参考記事
* [ぱふぅ家のホームページ [PHP] Dropbox API でファイル情報を取得](https://qiita.com/tksnino/items/c39b509494cc7f05954a#phpでdropboxapiを使う)
* [PHPで Dropboxのファイル取得](https://www.pahoo.org/e-soul/webtech/php06/php06-64-01.shtm)
* [SlackのWebhook URL取得手順](https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008)