５つのSlackアプリを立ち上げてみてわかったこと

昨年のSlack Advent カレンダーに参加して、２つのpostをしたのですが

https://qiita.com/geeorgey/items/656b59de6d738c740760

https://qiita.com/geeorgey/items/f117ee14a85c07ef8ea9

それの続きでSlackアプリの開発をすべく時間を使ってみた所、今年は３つのアプリをリリースすることが出来ました。

- OYASUMI bot https://lne.st/oyasumilink
- TIPS https://lne.st/tips
- TimeLine for Slack https://lne.st/wr21
- YOKOKU for Slack https://lne.st/4cdy
- TASUKARU-TaskALL- https://lne.st/3x1u


## 関連記事
https://qiita.com/geeorgey/items/e52cf443c6d991264f3c

https://qiita.com/geeorgey/items/473d5f387f11435be6db

## いくつか作る中での気付き

### blocksの生成は一つの関数にまとめておく
例えばHome画面の生成について。

```python:bolt.py
@bolt_app.event("app_home_opened")
def app_home_opened(client, event, body, logger, view):
    #なんらかの処理で以下のblocksを作る
    try:
        # Call views.publish with the built-in client
        client.views_publish(
            # Use the user ID associated with the event
            user_id=event["user"],
            # Home tabs must be enabled in your app configuration
            view={
                "type": "home",
                "blocks": blocks
            }
        )
    except Exception as e:
        logger.error(f"Error publishing home tab: {e}")  
```

こんな風にしてviews_publishすれば済むのですが、app_home_openedの中で作ってしまうと困ることがあります。
それは、例えばHome画面の中にボタン等を押して、状態変更するような仕組みを追加するとします。
処理の最後に保存ボタンを押すことになると思うのですが、その時にviewを更新したいということになるはずです。
例えばモーダルで処理をしていたとして、最後にボタンのsubmitを行う際にack()で閉じちゃったりすると、Homeタブ部分が更新されず古い情報が出たままになってしまいます。
その為、ブロック生成用の関数を作っておいてblocksを生成し、それを渡すようにする必要があります。
これはhome画面のみではなく、例えばモーダルの中身だけの処理を作るにしても同様です。
例えば、データのリストを表示するようなモーダルがあったとして、編集ボタン→編集する→保存ボタン押す→最初のモーダルに戻るとデータが最新になった状態のリストで表示されている、というような流れを作る場合にも必要です。
ボタンを押して @bolt_app.action("action_id") で受け取ったところでblocksを作成してしまうと、結局最後の保存ボタンを押す時に、同じblocks生成コードを作ることになるため冗長化してしまうのです。
ということで、blocksの生成が発生する場合は、あらかじめ関数に切り出しておくと良いよ、というお話でした。

### ユーザーとのやり取りをどうやってやるべきか
ユーザーサポートをどのようにやるべきかについてです。
例えば、余計なSCOPEをつけないということで厳密にusers:read.emailをつけなかったりすると、メールアドレスも取得できませんので、事実上ユーザーに問い合わせ窓口用のメールアドレスを作ってそこに連絡をもらうしかなくなります。
最初はそれでいいやと思っていたのですが、こちらから更新情報を通知したいと思った時にメールアドレスが無いと連絡が取れないことに気付き、users:read.emailのSCOPEを追加してインストールした人のメールアドレスを取得できるように変えました。
もう少しときが進んで、Slackアプリについてのやり取りがメール…？という部分に疑問を持つようになりました。どうせだったらSlackでやり取りが完結したほうが良いだろう、そういうことです。
私の場合は、そういう思考プロセスからアプリ内で完結するように仕組みを整えました。
これについては、共通する需要かもしれませんのでソースコードを公開してあります。
詳細はこちらを御覧ください。

https://qiita.com/geeorgey/items/5091e94943337e4e1781

### timezone と localeの話
Slackアプリは何も国内のみで使われる訳ではありません。
言語設定が必要だったり、そもそもタイムゾーンが違ったりと、ユーザー間の違いを吸収する必要があります。
この辺は、Slack認定開発者試験の範囲にも入っているので重要な項目です。

言語設定はこのような形で取得することが出来ます。
日本語の場合はlocaleに ja-JP と入ります。

```python:bolt.py
        client = WebClient(token=slack_installation.bot_token)
        try:
            userinfo = client.users_info(
                user=slack_installation.user_id,
                include_locale=True
            )
        except SlackApiError as e:
            print("Error fetching users_info: {}".format(e))
        locale = userinfo['user']['locale']
```

次にタイムゾーンです。
slackで出てくるのは基本的にタイムスタンプでUNIXタイムスタンプというものになっています。時間はUTCを基準にされています。基本的には、このUTC時間を使ってユーザー毎に設定してあるタイムゾーンを勘案し、時間の表示を出し分けるという使い方になります。

タイムゾーン自体は、先程のuserinfoの中に入っていて

```python:bolt.py
    time_zone = userinfo.get('user').get('tz')
```
で取得することが出来ます。

今現在の、自分のタイムゾーンでのdatetimeを取得したい場合はこうするし

```
datetime.now(timezone(time_zone))
```

slack_ts に入っているタイムスタンプを、自分のタイムゾーンに合わせたいときは

```
slack_ts_datetime = datetime.utcfromtimestamp(float(body['event']['event_ts']))
```

こんな風にしてまずタイムスタンプをdatetimeに変換する。
最終的には、それをタイムゾーンに合わせるのでこんな形で変換してやる。

```
datetime.utcfromtimestamp(float(body['event']['event_ts'])).astimezone(timezone(time_zone))
```
例えば、最終更新時間の表示みたいな機能があったりすると、こうやってユーザー毎にタイムゾーンをuserinfoから取得して時間を変換してやる必要があります。
最初ここがものすごく面倒に感じたのですが仕方ないですね。

### 微妙に違うリターン値
Slack APIを通じて値を取得していると、微妙に形式が違うリターン値が返ってきたりします。
例えば、あるところでは
body['user']['id']で取得していたユーザーIDが、body['user_id']という形式で返ってきたりします。
エラーが出るたびにレスポンスのJSONをパースして確認したりするのが非常に面倒なので、値の返し方は統一してほしいなと思いつつ、めちゃくちゃ大変そうだなとも思うのでこちらで頑張る必要があります。

### 開発には３つ(もしくは２つ)の環境が必要
通常は２つで良いと思います。
私の場合は、アプリをHerokuでホストしているのですが、Redisを使った処理を書いている部分で、ローカルで実行するとどうしてもパスの関係が変わってしまってうまく動かないという問題が解決出来なかったため３つ使っています。

1：本番環境 @ Heroku
2：テスト環境 @ Heroku
3：ローカル開発環境

ローカルで本番環境と同等のことが実現できるということであれば1と3のみで良いと思います。
一応本番反映まえに本番環境と同等の場所でテストを入れたいよねということであれば2を使う理由にはなります。


### インストール時にオンボーディングDMを送るには
この辺に書かれているんですけど、オンボーディングの機能をつけたほうが良いよということでした。

https://api.slack.com/best-practices/onboarding

これを実現するには、CallbackOptions をつける必要があります。

```Python:bolt.py
callback_options = CallbackOptions(success=success, failure=failure)
bolt_app = App(
    logger=logger,
    signing_secret=os.environ.get("SIGNING_SECRET"),
    installation_store=installation_store,
    raise_error_for_unhandled_request=True,
    oauth_settings=OAuthSettings(
        client_id=client_id,
        client_secret=client_secret,
        state_store=oauth_state_store,
        scopes=os.environ.get("SLACK_SCOPES"),
        user_scopes=os.environ.get("SLACK_USER_SCOPES"),
        callback_options=callback_options,
    ),
)
bolt_app.enable_token_revocation_listeners()
```

これを入れておくと、インストール成功時・失敗時の挙動を定義することが出来ます。

成功時はこのように定義しておくことで処理を実行させることができますので、その時にDMを送ってあげましょう。

```python:bolt.py
def success(args:SuccessArgs) -> BoltResponse:
    assert args.request is not None
    #ここに処理を書く

    #例えばuserinfoを取得するならこんな形
    client = WebClient(token=args.installation.bot_token)
    try:
        userinfo = client.users_info(
            user=args.installation.user_id,
            include_locale=True
        )
    except SlackApiError as e:
        print("Error fetching conversations: {}".format(e))
```
例はこのあたりを参照ください

https://github.com/geeorgey/vue_flask_template/blob/6d5427dcf4a4aafc599a2f2c60dd264f337ebe73/backend/bolt.py#L105

失敗時は余りやることがないのでこんな形です

```Python:bolt.py
def failure(args:FailureArgs) -> BoltResponse:
    print('failure ■□■□■□■□■□■□■□■□■□■□■□')
    print(vars(args))
    assert args.request is not None
    assert args.reason is not None
    return BoltResponse(
        status=args.suggested_status_code,
        body="Slackへのインストールが失敗しました。こちらのウィンドウを閉じてもう一度やり直して下さい。"
    )
```

###　スラッシュコマンドは立場が弱め
なぜかと言うと、例えばコマンドが衝突した場合、あとにインストールしたほうに設定を上書きされちゃうから、ということでした。
使うならグローバルショートカットにするか、ホームタブにボタンを設置すると間違いがない模様。

## まとめ
ということで、立て続けにアプリをリリースしてみて感じたことを書いてみました。
Slackアプリ開発にも慣れてきたので、質問があれば答えられるかもしれません。

## 宣伝
最初に紹介した５つのアプリ、是非使ってみてください！
感想お待ちしてます。

- OYASUMI bot https://lne.st/oyasumilink
- TIPS https://lne.st/tips
- TimeLine for Slack https://lne.st/wr21
- YOKOKU for Slack https://lne.st/4cdy
- TASUKARU-TaskALL- https://lne.st/3x1u

### OYASUMI bot
Googleカレンダーのスケジュールに「休み」等の、休暇判定キーワードが入っていた場合に、自動的にSlackをスヌーズ状態に変更します。
メンションがあった場合に、メンションした相手にその日は休暇ですというレスを付けます。
スタッフの休暇を気持ちよく過ごしてもらう為のアプリです。

### TIPS
Slack用のリマインダーアプリです。
デフォルト機能のリマインダーは、一つのスケジュールに一つのメッセージの設定しか出来ません。
このアプリでは、一つのスケジュールに複数のメッセージを登録し、ランダムで指定された時間にpostします。
チャンネル特有のノウハウ等をスケジュール登録しておくことで、自然と情報が浸透する状態を作ります。


### TimeLine for Slack
全ての公開チャンネルのpostを一つのチャンネルにまとめるタイムラインチャンネルを生成するアプリです。
一部のチャンネルのみを集めたミックスチャンネルを作成すると、自分が必要なチャンネルだけのタイムラインを作り出すことが出来ます。
オプション機能として、メッセージ転送時にDeepL翻訳を挟むことが出来ます。

### YOKOKU for Slack
OYASUMI botの応用です。
例えば「営業」というキーワードが入った予定だけを抽出したいと思ったことはありませんか？
カレンダーをいちいち検索するのは手間がかかります。
このアプリを用いれば、特定のキーワードが入った予定を、特定のチャンネルに流すことが出来ます。

### TASUKARU 
Slackで色々な人からメンションをもらって混乱したことはありませんか？
何か頼まれていたはずだけど思い出せない…　そんな状態からあなたを救います。
TASUKARUはSlack専用のタスクマネージャーです。
デフォルトのスレッドやメンション画面では、処理が終わったpostをアーカイブすることが出来ませんが、TASUKARUならそれが出来ます。
今アクションが必要なものだけに集中することができる、それがTASUKARUです。