[Slack] プライベートチャンネルのポスト(POST)を一括でダウンロードするスクリプト書いてみた

# Slackのポスト(POST)について

[ポスト](https://get.slack.help/hc/ja/articles/203950418-ポストを作成する) はSlackの機能の一つで、通常よりも長文のメッセージや、書式付きのメッセージを投稿する場合に利用できます。投稿されたポストは複数人が共有して編集することも可能です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/172826/369c2309-34da-9245-52c8-cc98c8cebe6b.png)

あるプライベートチャンネルに多数のポストがあり、過去のものを一括ダウンロードしておきたいと思ったのですが、[インポート & エクスポートの説明](https://get.slack.help/hc/ja/articles/204897248-Guide-to-Slack-data-exports) を見ると、プライベートチャンネルからのエクスポートは「プラスデータエクスポート プラスプラン」が必要なようでした。

[料金プラン](https://slack.com/intl/ja/pricing)を見るとその価格は￥1,600（アクティブなユーザ1名あたり、年払いの場合の月額費用）。スタンダードの2倍弱となっており、このエクスポートのためだけにアップグレードするのはちょっと難しいので、APIを使用して一括ダウンロードするスクリプトを作ってみました。

# 処理の流れとスクリプト

[API仕様](https://api.slack.com/web)を見たところ、プライベートチャンネルはAPIでは"group"となっており、以下の流れでポストの取得を実現できそうです。

1) 投稿したメッセージ一覧の取得にチャンネルのID(group id)が必要なため、まず groups.list メソッドでチャンネル一覧を取得
2) 一覧から、取得対象のグループをユーザが選択
3) 選択したチャンネルに対して、投稿されたメッセージの履歴を groups.history メソッドで取得
4) 各メッセージに対して、ポストがあればローカルファイルとしてダウンロード
5) groups.history メソッドでは一度に最大1000件しか取得できないため、続きがある場合(レスポンスに "has_more": true があれば)、 3) と 4)を繰り返す

この中で、4) の「ポストがあれば」の判定ですが、ポストはSlack内部的には「テキスト形式の添付ファイル」で実装されており、mimetype が'text/plain'である添付ファイルを全て取得するようにしました。APIレスポンスからは普通の共有ファイル(テキスト形式のファイルを添付した場合)と区別ができないので、共有ファイルもダウンロードされるようになっています。

作成したスクリプトが以下になります。

```python:getFilesFromPrivateChannel.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from slackclient import SlackClient
import urllib2
import re
import datetime, time

# Slackのトークン
slack_token = '<ここにトークンを設定>'

# プライベートチャンネル(チャンネルID)のリストを取得
def getPrivateChannelIdList(slack_client):
  result = []
  group_list = slack_client.api_call(
    'groups.list',          # プライベートチャンネル取得APIは groups.list
    exclude_archived=False, # レスポンスにアーカイブチャンネルを含めるか(含める場合にFalse)
    exclude_members=True    # レスポンスにユーザ一覧を含めるか(含める場合にFalse)
  )

  if group_list['ok'] == True: # 正常なレスポンス
    groups = group_list['groups']
    for i in range(len(groups)):
      group_name = groups[i]['name']
      print str(i) + ' : ' + group_name # グループ名を標準出力
      result.append(groups[i]['id'])    # グループIDをリストに追加
  else:
    print 'Error at groups.list API:',
    print group_list

  return result

# チャンネルに投稿されたメッセージを取得し、ポスト(テキスト形式の添付ファイル)があればファイル出力
def outputFilesFromChannel(slack_client, channel_id):

  latest_time = 9999999999.999999 # 厳密には現在時刻だがこれでもOK
  has_more = 1 # チャンネルに1000件を超える投稿があれば1000件ごとに繰り返す
  while has_more == 1:
    comment_list = slack_client.api_call(
      'groups.history',   # 投稿取得APIは groups.history
      channel=channel_id,
      latest=latest_time,
      count=1000          # 1度に取得できる件数は最大1000件
    )

    if comment_list['ok'] == True:
      for comment in comment_list['messages']: # 各投稿をチェック

        latest_time = comment['ts']
        if comment.has_key('subtype') and comment['subtype'] == 'file_share' and \
           comment.has_key('file') and comment['file'].has_key('mimetype') and \
           comment['file']['mimetype'] == 'text/plain':

          # ファイル共有があり、かつそのmimetypeがテキスト形式の場合のみ、添付ファイルを出力
          file_title = comment['file']['title']
          file_timestamp = datetime.datetime.fromtimestamp(comment['file']['timestamp'])
          out_file_name = file_timestamp.strftime('%Y-%m-%d_%H%M%S') + '_' + comment['file']['id'] + '.txt'

          # ダウンロードはAPIが無いので、ヘッダにtoken設定してurllib2で取得
          req = urllib2.Request(comment['file']['url_private_download'])
          req.add_header('Authorization', 'Bearer ' + slack_token)
          with open(out_file_name, 'wb') as myfile:
            myfile.write(urllib2.urlopen(req).read())
            print 'Output : ' + out_file_name + '(' + file_title + ')'

      if comment_list['has_more'] == False: # 1000件以下なら終了
        has_more = 0

    else:
      print 'Error at groups.history API:',
      print comment_list
      break

  return

if __name__ == '__main__':
    
  slack_client = SlackClient(slack_token)

  private_channel_list = getPrivateChannelIdList(slack_client) # チャンネル一覧取得

  if len(private_channel_list) > 0:
    print 'Select channel num(0-' + str(len(private_channel_list)-1) + '):',
    input = raw_input() # 標準入力から対象チャンネルを選択

    channel_id = private_channel_list[int(input)]
    outputFilesFromChannel(slack_client, channel_id) # 選択したチャンネルに対して、ファイル出力
  else:
    print 'private channel num is 0'
```

# 実行してみる

実行にはトークンが必要となるので、以下より取得します。未発行の場合は「Create token」で発行して下さい。
https://api.slack.com/custom-integrations/legacy-tokens

スクリプト10行目の <ここにトークンを設定> に設定し、スクリプトを実行します。[slackclient](https://github.com/slackapi/python-slackclient) がインストールされていない場合は先にインストールして下さい。

```shell
pip install slackclient
```

実行例：

```shell-session
$ python getFilesFromPrivateChannel.py
0 : team-general
1 : team-randam
2 : team-meeting
3 : test-a17
4 : test-a18
5 : test-a19
Select channel num(0-5): 1
Output : 2018-01-25_151642_F8ZCQA19V.txt(ポスト(POST))
Output : 2017-12-20_112936_F8GFXA4D9.txt(2017-12-21 - メモ)
Output : 2017-11-22_160327_F83BM9KEU.txt(2017-11-22 - メモ)
Output : 2017-10-25_143619_F7P27V5QE.txt(2017-10-26 - メモ)
$ cat 2018-01-25_151642_F8ZCQA19V.txt
{"version":100,"revision":139,"root":{"children":[{"type":"p","text":"・これが"},{"type":"p","text":"・ポスト"},
{"type":"p","text":"・POST"},{"type":"p","text":"・通常よりも"},{"type":"p","text":"・長めの"},{"type":"p","text"
:"・メッセージ"},{"type":"p","text":"・複数人で","formats":{"i":[0,5]}},{"type":"p","text":"・共有しての","formats
":{"i":[0,6]}},{"type":"p","text":"・編集も可能","formats":{"i":[0,6]}},{"type":"p","text":""}]}}
```

うまくダウンロードできたようです。ファイルはjson形式になっているので、必要に応じて整形して利用できます。