Slack / 写真が投稿されたら自動でGoogle Driveに保存する

Shunsuke Sawada

自分のメモ用にSlackに写真を投稿してますが、
管理しやすいように全ての写真をGoogle Driveに保存しました。

動的に取得したSlackの画像URLを利用して request モジュールでダウンロードするのですが、 NodeJSのストリームでハマったのでメモします。

Google Drive のフォルダIDをフォルダ名から取得する

Google Drive のフォルダはファイルと同等ですが、フォルダの中にファイルを入れるには
parents プロパティにフォルダIDを指定します。
まずはそのフォルダIDを取得する必要があります。

SCOPES で必要なスコープを指定しておきましょう。

  
Google Cloud Platform で Cloud Functions を新規作成すると自動で Service Account が1つ作成されます。ローカルで開発する場合は、そのアカウントの認証情報をJSONファイルでダウンロードし GOOGLE_APPLICATION_CREDENTIALS という環境変数にそのパス設定しておきます。そうすると googleapis が自動で認証情報を読み取ってくれます。
※ GCP上では自動で設定されてますので、環境変数は必要ありません。

  
また、Service Account には自動でメールアドレスが付与されていますので、
Driveのフォルダの共有設定からメールアドレスを登録しましょう。Service Account はリアルなユーザーではないですが、共有設定をすることで、認証情報を利用すれば自由にアクセスが可能になります。

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const request = require('request');
const { google } = require('googleapis');

const SCOPES = [
  'https://www.googleapis.com/auth/drive',
];

const FOLDER_NAME = 'FromSlack';
const auth = await google.auth.getClient({ scopes: SCOPES });
const drive = google.drive({ version: 'v3', auth });

drive.files.list({
  corpora: 'allDrives',
  includeItemsFromAllDrives: true,
  supportsAllDrives: true,
  pageSize: 1,
  q: `name='${FOLDER_NAME}'`,
  fields: 'files(id, name)',
}, (driveError, driveRes) => {
  if (driveError) return console.log('The API returned an error: ' + driveError);
  const files = driveRes.data.files;
  if (files.length) {
    const folder = files.find(file => file.name === FOLDER_NAME);
    // IDが取得できました。
    console.log('Folder id: ', folder.id);
  }
}

Slack Event Subscriptions

Slackのメッセージ投稿は監視することができます。
メッセージが投稿されるたびに、指定したURLへPOSTリクエストを投げてくれます。

Screen_Shot_2019-10-20_at_21.41.22

リクエスト先のURLを設定

Slack API > Event Subscriptions > Request URL

Subscribe to Workspace Events

何のイベントを監視したいかを指定します。
message.channels を追加しましょう。

Screen_Shot_2019-10-20_at_21.42.32

このSlackアプリの権限を追加

Slack API > OAuth & Permissions > Scopes

Screen_Shot_2019-10-20_at_21.42.48

これでSlackにメッセージを投稿する度に Request URL へPOSTが送られます。

画像URLを取得

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
exports.index = async (req, res) => {
  res.sendStatus(200);

  const { type, event } = req.body;
  const { files } = event;

  if (type !== 'event_callback') {
    return res.status(500).send('This event is not valid');
  }

  slackFile = files ? files[0] : undefined;
  if (!slackFile) {
    return res.status(500).send('No file from Slack');
  }

  console.log(slackFile.url_private)
}

直後に res.sendStatus(200);200 を返していますが、これをしないとSlackは3回ほどリトライを繰り返します。ちゃんとPOSTがリクエストを受け取ったことをSlackに伝えるための処理です。
参考:
https://stackoverflow.com/questions/50715387/slack-events-api-triggers-multiple-times-by-one-message

画像ファイルのURLは url_private で取得できます。

画像をダウンロードする

Slackのアクセストークンが必要なので取得しておきましょう。
Slack API > Installed App Settings > OAuth Access Token

encoding: null を指定して bodyBuffer で受け取れるようにしています。
この指定がないと body はテキストとなります。

js
1
2
3
4
5
6
7
8
9
10
11
const request = require('request');
  ...
  const stream = request({
    url: slackFile.url_private,
    method: 'GET',
    encoding: null,
    headers: {
      'Authorization': `Bearer ${process.env.SLACK_ACCESS_TOKEN}`,
      'Content-Type': 'application/json; charset=utf-8',
    },
  });

request の返り値は stream ですが、それをそのまま Google Drive へのアップロードに使えます。 request だと第2引数のコールバックをよく使用すると思いますが、今回はちょっと違います。

Google Drive にアップロード

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const { PassThrough } = require('stream');
const { google } = require('googleapis');

  ...
  const auth = await google.auth.getClient({ scopes: SCOPES });
  const drive = google.drive({ version: 'v3', auth });

  ...

  const stream = request({
    ...
  });

  const pass = new PassThrough()
  stream.pipe(pass);

  drive.files.create({
    requestBody: {
      parents: [folder.id], // 取得したフォルダID
      mimeType: slackFile.mimetype,
      name: slackFile.name,
    },
    media: {
      mimeType: slackFile.mimetype,
      body: pass,
    },
    fields: 'id',
  }, (fileError, file) => {
    if (fileError) {
      console.error(fileError);
      returnres.status(500).end();
    } else {
      console.log('File: ', file);
      res.status(200).end();
    }
  });

ここがちょっと変わってるのですが、これがないとファイルの中身が空っぽになってしまいました。

js
1
2
const pass = new PassThrough()
stream.pipe(pass);

参考:
https://stackoverflow.com/questions/19553837/node-js-piping-the-same-readable-stream-into-multiple-writable-targets/40874999#40874999
https://github.com/aws/aws-sdk-js/issues/1277

  
  
以上でSlackの投稿を自動でDriveに保存できます。
ユーザーIDやSlackメッセージを検索用フィールドに保存すると、さらに使い勝手が良くなるほと思います。

  

3
Shunsuke Sawada

おすすめの記事

エンジニアの人いろいろ
Slack / Google Cloud Platformと連携してスラッシュコマンドを作成する
Slack / Functions FrameworkでサクッとCloud Functionsをカスタマイズ