続：Slack Bolt for Pythonを使ってのbot構築 アプリ配信に向けた対応

![Slack___Slack_API__Applications___Leave_a_Nest_Co__ltd_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22294/84c999de-175a-b0a9-13c3-0c3d00b90b3f.png)
前回はSlack Advent Calendar2020の三日目に書きました。
[Slack Bolt for Pythonを使ってのbot構築。ローカル開発からHerokuデプロイまで
](https://qiita.com/geeorgey/items/656b59de6d738c740760)

本日はここから少し進めて、開発したアプリを複数組織にインストールして使えるようにするためにどうしたら良いのかについて深堀りしていきます。

## boltの生成方法が変わる
[参考ソースはこちらです](https://github.com/slackapi/bolt-python/blob/v1.1.1/examples/sqlalchemy/oauth_app.py)


前回書いたコードは以下の通りです。一つの組織でのみ使えれば良い場合はこの形で問題ありません。トークンはBot token です。

```
app = App(
    token=os.environ.get("SLACK_BOT_TOKEN"),
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET")
)
```
複数組織へのインストールが必要な場合はこんな感じにします。

```app.py
import sqlalchemy
from sqlalchemy.engine import Engine

#ここは使うDBによって変えて下さい
database_url = "sqlite:///slackapp.db"
# database_url = "postgresql://localhost/slackapp"  # pip install psycopg2

logger = logging.getLogger(__name__)
client_id, client_secret, signing_secret = (
    os.environ["SLACK_CLIENT_ID"],
    os.environ["SLACK_CLIENT_SECRET"],
    os.environ["SLACK_SIGNING_SECRET"],
)

engine: Engine = sqlalchemy.create_engine(database_url)
installation_store = SQLAlchemyInstallationStore(
    client_id=client_id, engine=engine, logger=logger,
)
oauth_state_store = SQLAlchemyOAuthStateStore(
    expiration_seconds=120, engine=engine, logger=logger,
)

#slack_installationsテーブルにレコードがなければapp.py起動時にテーブルを初期化
try:
    engine.execute("select count(*) from slack_installations")
except Exception as e:
    installation_store.metadata.create_all(engine)
    oauth_state_store.metadata.create_all(engine)

app = App(
    logger=logger,
    signing_secret=signing_secret,
    installation_store=installation_store,
    oauth_settings=OAuthSettings(
        client_id=client_id,
        client_secret=client_secret,
        state_store=oauth_state_store,
    ),
)

@flask_app.route("/slack/install", methods=["GET"])
def install():
    return handler.handle(request)


@flask_app.route("/slack/oauth_redirect", methods=["GET"])
def oauth_redirect():
    return handler.handle(request)
```

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">複数の team からリクエストが来るわけなので、起動時に team の確定はできません。こんな感じで InstallationStore を使ってもらえれば、インストール時とアプリの実行時の判定の連携をやってくれます。 <a href="https://t.co/HszjchEwE0">https://t.co/HszjchEwE0</a><a href="https://t.co/ZDfcoQJKb1">https://t.co/ZDfcoQJKb1</a></p>&mdash; Kazuhiro Sera (瀬良) (@seratch_ja) <a href="https://twitter.com/seratch_ja/status/1336180386636808192?ref_src=twsrc%5Etfw">December 8, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ということで、よしなにしてくれるようになります。
おなじみの、say(f"Hi Yo, <@{user}>!")
なんかはこのような形で受けておかないと、飛んできたメッセージのteam_idがわからなくなってしまい、sayレスポンスするとchannel_not_foundエラーが出ます。そりゃそうです、初期状態ではtokenが最初のワークスペースのインストール時のものに固定しているのですから。
>The server responded with: {'ok': False, 'error': 'channel_not_found'})


##渡ってくるデータについて

```appmention.py
@app.event("app_mention")
def handle_mention(context,body,say,logger):
```

こんな感じで、引数を受け取ると思います。個人的には、ここで何が受け取れるのかを知りたかったのですが、ドキュメントにはなっていないようです。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">そういうのがないんですよねぇ。。こんな感じの middleware を仕込んで出力を見てもらうか <a href="https://t.co/AXKw0R2XWx">https://t.co/AXKw0R2XWx</a> Java SDK のクラス定義を見てもらうのが確実です。 <a href="https://t.co/LxQlGC3BCT">https://t.co/LxQlGC3BCT</a></p>&mdash; Kazuhiro Sera (瀬良) (@seratch_ja) <a href="https://twitter.com/seratch_ja/status/1334780696347332609?ref_src=twsrc%5Etfw">December 4, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ということで、地道なリサーチが必要ですが、使えそうなものをピックアップしておきます。
[Contextとは？こちらを参照下さい。](https://slack.dev/bolt-python/concepts#context)
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">あぁ、うっかりしてましたが、このユースケースなら context をリスナーの引数に追加して <a href="https://t.co/bsiVAnSzSP">https://t.co/bsiVAnSzSP</a>_id で大丈夫なはずです！</p>&mdash; Kazuhiro Sera (瀬良) (@seratch_ja) <a href="https://twitter.com/seratch_ja/status/1334818564214116353?ref_src=twsrc%5Etfw">December 4, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ということで、contextを受けておけば、以下のデータが取得できます。
tokenなんかはcontextからじゃないと取れなさそうですね。

```app_mention_context.py
{
	'team_id': 'xxxxxxx', 
	'user_id': 'xxxxxxx', 
	'channel_id': 'xxxxxxxx', 
	'logger': <Logger app.py (DEBUG)>, 
	'token': 'xoxb-xxxxxxxxxxxxxxx', 
	'client': <slack_sdk.web.client.WebClient object at xxxxxxx>,
	'authorize_result':
		{'enterprise_id': None, 'team_id': 'xxxxxxx', 'bot_user_id': 'xxxxxx', 'bot_id': 'xxxxxx', 'bot_token': 'xoxb-xxxxxxxxxxxxxxx', 'user_id': None, 'user_token': None},
	'bot_id': 'xxxxx',
	'bot_user_id': 'xxxxx',
	'bot_token': 'xoxb-xxxxxxxxxxxxxxx',
	'ack': <slack_bolt.context.ack.ack.Ack object at xxxxx>, 
	'say': <slack_bolt.context.say.say.Say object at xxxxx>, 
	'respond': <slack_bolt.context.respond.respond.Respond object at xxxxx>
}
```

bodyはこんな感じ。メッセージの中身はbodyを参照しましょう。

```app_mention_body.py
{
	'token': 'xxxxxxxxxx', 
	'team_id': 'xxxxxxxxxxxxxxx',
	'api_app_id': 'xxxxxxxxxxxxxxx', 
	'event': 
		{
			'client_msg_id': 'dada87aa-xxxxxxxxxxxxxxx', 
			'type': 'app_mention', 
			'text': '<@xxxxxxx>\xa0メンションメッセージ',
			'user': 'xxxxxxxxxxxxxxx',
			'ts': '1607487863.008600',
			'team': 'xxxxxxxxxxxxxxx',
			'blocks': [{'type': 'rich_text', 'block_id': 'XFYvi', 'elements': [{'type': 'rich_text_section', 'elements': [{'type': 'user', 'user_id': 'xxxxxxxxxxxxxxx'}, {'type': 'text', 'text': '\xa0メンションメッセージ'}]}]}], 
			'channel': 'xxxxxxxxxxxxxxx', 
			'event_ts': '1607487863.008600'
		}, 
	'type': 'event_callback',
	'event_id': 'xxxxxxxxxxxxxxx',
	'event_time': 1607487863, 
	'authorizations': [{'enterprise_id': None, 'team_id': 'xxxxxxxxxxxxxxx', 'user_id': 'xxxxxxxxxxxxxxx', 'is_bot': True, 'is_enterprise_install': False}], 
	'is_ext_shared_channel': False,
	'event_context': '1-app_mention-xxxxxxxxxxxxxxx-xxxxxxxxxxxxxxx'
}
```

アプリのインストール時にスコープを設定する必要があります。.envに環境変数に入れておきましょう。Herokuデプロイ時は環境変数設定に入れておいて下さい。ここで書いたものは一例です。

```env.
export SLACK_SCOPES=app_mentions:read,channels:history,channels:join,channels:read,chat:write,chat:write.public,files:write,im:history,reactions:read,team:read,users:read
```
これで、インストール時に適切なパーミッションを含んだbotがインストールされます。

まずは、最初にインストールしたワークスペースで動作を確認しましょう。問題なく動いたら、マルチワークスペース対応に進みます。
アプリの配信設定をしましょう。
まずはOAuth＆Permissions画面に行ってからRedirect URLsを変更します。
>https://xxxxxx.ngrok.io/slack/oauth_redirect

/slack/oauth_redirect
の部分が元のapp.pyにはありませんでした。
これが出来たら、SlackのManage Distributionを開いて書いてある事にチェックを入れていきます。
固有の設定値をハードコーディングしてないか？
通信はhttpsになってるか？
みたいなものをクリアすると、晴れてdistribution可能な状態になります。

![Slack___Slack_API__Applications___Leave_a_Nest_Co__ltd_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22294/82e5c340-6ecb-e1f8-e3e6-9407780adaed.png)

アプリの管理画面の Sharable URL は [OAuth の state パラメータ](https://auth0.com/docs/protocols/state-parameters)がないので Bolt for Python を使っているときはそのまま使えません。アプリのドメインのあとに `/slack/install` のパスをつけた URL からお望みのワークスペースへとインストールしてください。

あとは、複数ワークスペースでも問題なくbotとのやり取りが出来るかを確認しましょう！

ということで、やり方が分かってしまえば、シンプルにまとまってしまうのですが、理解に至るまでが時間がかかったよというお話でした。参考になれば幸いです！

多大な感謝を @seratch さんに。

## 関連リンク
- [pythonを使ってリアクティブなWebアプリを作りたい【開発編5】Slack boltの組み込み](https://qiita.com/geeorgey/items/ca2942ff054e30587c78)