News API を使ってニュースを Slack で読んでみよう

こんにちは、Slack で Developer Relations を担当している Jason と申します。🙇🏻‍♂️

全国的に桜が咲き始めてきて、春めいた気分になってきましたね！春といえば「何か新しくチャレンジしよう💪🏻」という気持ちになりますが、皆さんはどうでしょうか？もし何か Slack API を使って新しいことをしてみたいと思っているなら、ワークフロービルダーに自前のステップを追加してカスタマイズしてみるのはどうでしょう？

このチュートリアルでは、ワークフロービルダーのステップを作成します。このステップをワークフローに追加すると、指定したチャンネルにニュース記事が送信されるようになります。例として、以下のようなメッセージが投稿されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550365/c039d6c7-5cd3-8aa9-f460-ff8ac2f5aa1a.png" width=700>

# ワークフロービルダーとは？

ワークフロービルダーは Slack 内でタスクを標準化・自動化するために利用できるノーコードツールです。ワークフローは、処理の開始イベントであるトリガーと複数のステップで構成されます。あらかじめ用意されている組み込みのステップは以下の二つです。

1. メッセージを送信する（メッセージステップ）
2. フォームを送信し、情報を収集する（フォームステップ）

これらの組み込みのステップに加えて、自分で開発した Slack アプリによる :sparkles: **カスタムステップ** :sparkles:を使用することもできます。このチュートリアルでは、このカスタムステップを作成します。

なお、その前にワークフロービルダーをもっと知りたい方は[ヘルプページのドキュメント](https://slack.com/intl/ja-jp/help/articles/360035692513-%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%83%93%E3%83%AB%E3%83%80%E3%83%BC%E3%82%AC%E3%82%A4%E3%83%89)をお読みください。

開発を始める前に Slack でどのようなニュースが配信されると便利かを考えてみましょう。

# ユースケースを考えてみる

* 毎日、特定のトピックに関するニュース記事を読みたい。例えば・・
    * 業界の動向（新技術のトレンド、法改正、日経平均、決算発表など）
    * 新型コロナウイルスへの対策や感染状況の最新情報
    * 自分の企業や製品に関する言及（いわゆるエゴサーチ）

* 最新のスポーツニュースを投稿して #baseball のような趣味のチャンネルの会話を盛り上げたい
    * プロ野球、読売ジャイアンツ、巨人
    * フィギュアスケート、羽生結弦、宇野昌磨
    * 高校野球、甲子園
* 毎朝、特定のジャンルに限らず、最新なニュースを投稿してほしい

# 必要なもの
- Slack アプリの開発に使うワークスペース（ワークフロービルダーは無料プランでは利用できないため、有料プランのワークスペースが必要です）
- News APIのアカウント（[こちら](https://newsapi.org/register)からサインアップして作成できます）
- カスタムステップを提供する Slack アプリ
- Python の実行環境と [Bolt for Python](https://slack.dev/bolt-python/ja-jp/tutorial/getting-started)

# 実装の流れ

完成した Slack アプリは、以下のように動作します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550365/35de2029-05d1-c9a6-6f02-fad9320651d5.gif" width=600>

次にコードの実装をご紹介します。

## GitHub のリポジトリ

以下の GitHub リポジトリでコードを公開しています。この記事でのコード例は、わかりやすくするために一部を省略しています。完全な実装はこの GitHub リポジトリのコードを参考にしてください。

https://github.com/wongjas/news-api-for-slack

## News API キーの取得

[News API](https://newsapi.org/) の API キーの取得はとても簡単です。個人での利用を目的に使用する場合であれば **I am an individual (私は個人です)** を選択してアカウントを登録すれば API キーが付与されます。登録に必要な情報は、氏名、メールアドレス、パスワードだけです。なお、この記事の投稿時点で News API の無料プランでは 1 日あたり最大 100 件のリクエストを送信することができます。この値は後で `NEWS_API_KEY` という環境変数として設定します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550365/9c587fde-dbc8-57e4-1ada-e5e564cc6ab8.png" width=400>

## Slack アプリの作成・設定

[このリンク](https://api.slack.com/apps?new_app=1&manifest_yaml=display_information%3A%0A%20%20name%3A%20News%20in%20Slack!%0A%20%20description%3A%20Slack%20%E3%81%A7%E3%83%8B%E3%83%A5%E3%83%BC%E3%82%B9%E3%82%92%E8%AA%AD%E3%82%82%E3%81%86%EF%BC%81%0Afeatures%3A%0A%20%20bot_user%3A%0A%20%20%20%20display_name%3A%20NewsBot%0A%20%20%20%20always_online%3A%20false%0A%20%20workflow_steps%3A%0A%20%20%20%20-%20name%3A%20%E3%80%8CNews%20in%20Slack!%E3%80%8D%E3%81%AE%E3%82%B9%E3%83%86%E3%83%83%E3%83%97%0A%20%20%20%20%20%20callback_id%3A%20news_step%0Aoauth_config%3A%0A%20%20scopes%3A%0A%20%20%20%20bot%3A%0A%20%20%20%20%20%20-%20chat%3Awrite%0A%20%20%20%20%20%20-%20chat%3Awrite.public%0A%20%20%20%20%20%20-%20workflow.steps%3Aexecute%0Asettings%3A%0A%20%20event_subscriptions%3A%0A%20%20%20%20bot_events%3A%0A%20%20%20%20%20%20-%20workflow_step_execute%0A%20%20interactivity%3A%0A%20%20%20%20is_enabled%3A%20true%0A%20%20socket_mode_enabled%3A%20true%0A )から Slack アプリを作成することができます。このリンクから作成すると、Slack アプリが機能するために必要なスコープや設定が自動的に追加されます。残るは、どのワークスペースにこの Slack アプリを置くかを選択するだけになります。

次に WebSocket 経由で Slack とのペイロードを送受信できる機能である[ソケットモード](https://qiita.com/seratch/items/1a460c08c3e245b56441)を使用するための **App-Level トークン** を発行する必要があります。

Slack アプリ管理画面の **Basic Information** ページから **App-Level Tokens** までスクロールし、**Generate Token and Scopes**（トークン生成とスコープ） ボタンを押します。このトークンに好きな名前を付けて `connections:write` というスコープを付与してください。 最後に、**Generate** を押します。`xapp-` から始まる文字列のトークンの値が表示されるはずなので、その値をコピーして保管しておいてください。後ほど、 `SLACK_APP_TOKEN` として環境変数に設定します。

次に、Slack アプリをワークスペースにインストールします。こうすることでアプリがワークスペース内で追加されます。そして、発行されたボットトークンを使って Slack の Web API の呼び出しに使うことができます。

Slack アプリの管理画面の左側のペインにある **Install App** ページにアクセスしてください。その後、**Install to Workspace** ボタンを押し、Slack アプリをインストールします。リダイレクトされた後、`xoxb-` から始まる **Bot User OAuth Token** の値をコピーしてください。これを `SLACK_BOT_TOKEN` として環境変数に設定します。`SLACK_APP_TOKEN` と `SLACK_BOT_TOKEN` は似ていますが、間違えないようにご注意ください。

## Pythonの環境と環境変数の設定

GitHub リポジトリからアプリのプロジェクトをチェックアウトしたら、Python 環境をセットアップしましょう。

 `requirements.txt` を使用して、このプロジェクトに必要な Python のライブラリをインストールします。また、ここまでの手順で取得しておいた 3 つのトークンを環境変数に設定してください。

```
# Pythonの環境設定
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# 上記の手順で取得した API　キーとトークンを環境変数としての設定
export NEWS_API_KEY=as2fs...
export SLACK_APP_TOKEN=xapp-...
export SLACK_BOT_TOKEN=xoxb-...
```

最後に、アプリを起動するにはこちらのコマンドを使ってみてください。

```py
python app.py
```
## カスタムステップを構成する 3 つの関数
プロジェクトの準備ができました。ここからはカスタムステップの実装を解説していきます。

カスタムステップを提供するためには、ここから説明する `edit`、`save`、`execute` という 3 つの関数を実装する必要があります。一つずつ説明していきます。

### ワークフロービルダーのステップ編集画面を表示（`edit`）

`edit` という関数は、ワークフロービルダーでワークフローを作成しているユーザーがこのカスタムステップを追加して、その設定画面を開いたときに呼び出されます。今回のアプリでは、以下のようにニュースの取得する条件と通知先のチャンネルを指定します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550365/1b3bd5cd-8af0-4cf9-2f4d-de342d799248.png" width=500>

次にこの画面の実装を説明します。このモーダルの見た目を調整するために [Block Kit Builder](https://api.slack.com/tools/block-kit-builder) を使うと便利でしょう。このモーダルがどのように表示し、ユーザから情報を収集するかのプロトタイプを作ることができます。

この関数はユーザが最初にステップの設定をするときだけでなく、その後にステップの設定を変更するときにも呼び出されることに注意してください。この編集のユースケースをうまく対応するためには、このモーダルの入力ブロックに前回保存した設定内容をあらかじめ初期値として渡す必要があります。以下のコード例は、ニュースの検索条件のブロックでの実装例です。

```py
def edit(ack: Ack, step: dict, configure: Configure):
    ack()
    inputs = step.get("inputs")
    blocks = []

    # ニュースを検索する条件を指定するブロック
    query_block = {
        "type": "input",
        "block_id": input_query,
        "optional": True,
        "element": {
            "type": "plain_text_input",
            "action_id": "_",
            "placeholder": {
                "type": "plain_text",
                "text": "例：東証、テレワーク、Jリーグ（カンマ / 読点区切りで複数指定可能）",
            },
        },
        "label": {
            "type": "plain_text",
            "text": "検索条件（指定しない場合は全記事から最新を取得）",
        },
    }

    # 一度保存された後に再編集するときに、現在保存されている値を初期値として設定
    if input_query in inputs:
        value = inputs.get(input_query).get("value")
        if value is not None:
            query_block["element"]["initial_value"] = value

    # モーダル内の blocks に追加（追加した順に表示されます）
    blocks.append(query_block)

    #  configure は blocks を組み立てること以外の全てをやってくれるユーテリティです
    configure(blocks=blocks)
```

なお `edit` 関数についてより詳細な情報を知るには、[Bolt の公式ドキュメント](https://slack.dev/bolt-python/ja-jp/concepts#adding-editing-steps)を参照してください。

### ワークフロービルダーのステップ設定変更の保存（`save`）

`save` という関数は、ワークフロービルダー内で `edit` が応答したモーダルを使ってその **保存する** ボタンを押したときに呼び出されます。入力内容をチェックして、問題なければその値の保存を Slack に対して依頼します。もし不備があればエラーメッセージで応答することができます。

`inputs` という辞書型のオブジェクトは、このステップが実行されたときに `execute` 関数が入力として受け取るデータを定義します。一方、`outputs` は、このステップが正常に完了したときに後続のステップに引き渡す値のリストです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550365/47ba8d9c-6ec2-e89e-f655-d9a42f4d03a7.gif" width=500>

```py
def save(ack: Ack, view: dict, update: Update):
    state_values = view["state"]["values"]

    # 送信された入力値を取得（ここではニュースを検索する条件として指定されたカンマ区切りのキーワード）
    query = _extract(state_values, input_query, "value")

    update(
        inputs={
            # このステップが実行されたときに渡される入力値を定義
            input_query: {"value": query},
        },
        # このステップの実行が正常に完了したときに後続に渡す変数を定義
        outputs=[
            {
                "name": channel_id,
                "type": "text",
                "label": "投稿されたメッセージの時刻",
            }
            for channel_id in channels
        ],
    )
    ack()
```

なお `save` 関数についてより詳細な情報を知るには、[Bolt の公式ドキュメント](https://slack.dev/bolt-python/ja-jp/concepts#saving-steps)を参照してください。

###  ワークフローの一部としてステップの処理を実行（`execute`）

`execute` という関数は、これまでのワークフロービルダーの設定画面のハンドリングとは異なり、このステップがワークフローの中で実行されたときに呼び出されるものです。ワークフロービルダーを使って作ったワークフローが公開されて、エンドユーザーがこのステップを含んだワークフローを実行したとき（あるいは定期実行されたとき）に都度実行されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550365/35de2029-05d1-c9a6-6f02-fad9320651d5.gif" width=600>

今回の Slack アプリでは、指定された検索条件で News API を呼び出して、返されたニュース記事を Slack メッセージとして送信します。

```py
def execute(step: dict, client: WebClient, complete: Complete, fail: Fail):
    inputs = step.get("inputs", {})
    try:
        # inputs から値を取り出します
        query = inputs.get(input_query).get("value")
        num_articles = int(inputs.get(input_num_articles).get("value"))
        language = inputs.get(input_language).get("value")
        channels = inputs.get(input_channel_ids).get("value").split(",")

        # 上記の値を使って外部の API の News API を呼び出します
        articles = fetch_articles(news_api_key, query, num_articles, language)
    except Exception as err:
        fail(error={"message": f"Failed to fetch news articles ({err})"})
        return

    outputs = {}
    try:
        if articles:
            for article in articles:
                blocks = format_article(article)
                for channel in channels:
                    # あらかじめ指定されたすべてのチャンネルにメッセージを送信します
                    response = client.chat_postMessage(
                        channel=channel,
                        blocks=blocks,
                        unfurl_links=False,
                        unfurl_media=False,
                        text=article.title,
                    )
                    outputs[channel] = response.get("message").get("ts")
        else:
            # 今回はニュース記事が見つからなかった旨を通知します
            for channel in channels:
                response = client.chat_postMessage(
                    channel=channel, text=f"現在「{query}」に一致する記事はありません。"
                )
                outputs[channel] = response.get("message").get("ts")

    except SlackClientError as err:
        fail(error={"message": f"Notification failed ({err})"})

    # complete に 後続ステップが使う変数のリストである outputs を渡すことでこのステップの実行が正常終了します
    complete(outputs=outputs)
```

この `execute` 関数についてより詳細な情報を知りたい場合は [Bolt のドキュメント](https://slack.dev/bolt-python/ja-jp/concepts#executing-steps)を参照してください。

## カスタムステップを含むワークフローを作成

カスタムステップの準備ができました！このステップを使ったワークフローを作ってみましょう。

ワークフローの作成方法の詳細は、[ヘルプページ](https://slack.com/intl/ja-jp/help/articles/360053571454-Slack-%E3%81%A7%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)を参考にしてください。

どのようなトリガーを使っても構いませんが、簡単に動作確認できるように、まずはショートカットのトリガーを使って作成してみてください。新しいワークフローができたら、そこに今回作ったカスタムステップを追加しましょう。

設定が終わったらワークフローを公開するのもお忘れなく。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550365/dbd22a57-fe0c-da40-1b01-741fe077234c.gif" width=600>

# ワークフローの実行

ワークフローを公開したらショートカットを実行するために指定したチャンネルから呼び出してみましょう。メッセージ送信エリアにある稲妻のアイコンをクリックして、そこで **最新のニュース記事を投稿** というショートカットを探して選択します。 

正常に動作した場合は、指定のチャンネルにニュース記事が配信されるはずです！もし何も起きなかった場合は何かエラーが発生しているかもしれません。ワークフロービルダーを開いて、ワークフローの「アクティビティ」タブでエラーログを確認してください。

うまく動いた場合は、トリガーを定期実行に切り替えてみましょう。ワークフローの設定を export して再利用できます。定期実行のトリガーで、たとえば「平日の 12:00 に実行する」などのトリガーで運用してみるとよいかもしれませんね。

# このアプリのさらなるカスタマイズ

今回は、News API という外部の API と連携したワークフローステップの実装方法をご紹介しました。このチュートリアルで学んだことからさらに発展させれば、以下のようなアイデアを実現できるかと思います。

- 今回のアプリにホームタブを追加して、チャンネル以外でもニュースを見られるようにする
- [News API の豊富なオプション](https://newsapi.org/docs)を活用して、より細かい設定項目をカスタムステップに追加する
- このニュース配信をワークフロービルダー以外でも使えるようにする

それでは、またお会いしましょう :wave: