SlackでTwitterがやりたい!


はじめに

Slackアドベントカレンダー17日目です!
今回は,業務中にもTwitterをするために,Slack上でTwitterを行うためのSlack Appを実装してみます.

できたもの
今の所Slack上から以下の機能が使用可能です.
前日に咳き込みながら急ピッチで作ったので,これだけで勘弁してください(^_^;)

  • タイムラインの閲覧
  • tweet
  • favo
  • リツイート

Slack上のフォームにメッセージを入力し送信すると,Tweetが投稿されます.


入力して,送信ボタンを押す.

実際に,Twitter上にも投稿されます!(SlackでのURLの表示など課題は残っています.)
同様に,ファボ,リツイートも可能です.

作り方

コードはGitHub上で公開しています.

GitHub:takeru56 / slack_tweet

使用技術

Slack

  • Home Tab
    タイムラインの表示箇所

  • Block Kit
    UIの作成

  • Web API
    メッセージの投稿
    最新の投稿の情報を取得

  • Event API
    Home Tabへのアクセス
    Tweetの検出

  • Interactive component
    ボタンのクリック

プログラミング

  • Ruby
  • Sinatra
  • twitter api / slack api

1. 準備

Slack Appを作成してください.

Scope

使用するスコープです.
サイドバー > Features > OAuth & Permisson > Scopeから設定を済ませておきます.

Event

使用するイベントのタイプです.
サイドバー > Event Subscriptionsから設定を済ませておきます.

同時に,Request URLを登録しておきます.
残念ながらEvent apiのコールバックで指定することができるURLはhttpsに限られています.
ngrokといったポートフォワーディングサービスを使用することで,ローカルで開発を完結することができます!登録は必要ですが,無料で使用可能です.

ngrok.yml
authtoken: [取得したauth tokenを記載する]
tunnels:
  sinatra:
    proto: http
    addr: 4567

登録が完了したら,取得したauth tokenをngrok.ymlに記載して,$ngrok start -config ngrok.yml --all --region=jpで起動します.

サーバとngrokを立ち上げ,ngrokのコンソール画面に表示されているhttpsのURLを入力すれば,Verifiedが表示され,登録が完了するはずです!

Interactive Component

ボタンが押されたときのEventを受け取ります.
ボタンやピッカーといったコンポーネントに対するイベントはEvent APIとま分離されているようなので,
別途URLを設定しておきます.

Home Tab

今回は,タイムラインを投稿する場としてHomeTabを使用しました.
普通のチャンネルでも良かったのですが,HomeTabは個別に割り当てられており,Tabを開くイベントも提供されていることから,Tabを開くタイミングでロードを行えば,タイムラインの表示にぴったりだなと感じました.
サイドバー Features > App HOME からHome Tabを有効にしておきます.

Twitter API

Twitter APIを使用するのでそちらの登録も済ませておきます.

環境変数

少し多いですが,以下6つの環境変数を設定しておきます.
slack

  • SLACK_VERIFICATION_TOKNE
  • SLACK_ACCESS_TOKEN

twitter

  • TWITTER_CONSUMER_KEY
  • TWITTER_CONSUMER_SECRET
  • TWITTER_ACCESS_TOKEN
  • TWITTER_ACCESS_TOKEN_SECRET

2. タイムライン

ここからは実際にサーバを立てて処理を書いていきます.

Slackにタイムラインを表示する流れはとてもシンプルです.まずはHomeTabを開くイベントを待ち受ける処理を書きます.イベントが発生すると先程指定したURLにリクエストが飛んできます.飛んできたリクエストに対してイベントごとに振り分ける処理を書いてあげます.

app.rb
post '/event' do
  params = JSON.parse(request.body.read)
  return  {challenge: params["challenge"]}.to_json if params["type"] == 'url_verification'
  # accept event only from slack
  return if params["token"] != ENV["SLACK_VERIFICATION_TOKEN"]

  if params["event"]["type"] == "app_home_opened" && params["event"]["tab"] == "messages"
    handle_messages_opened(params)
  elsif params["event"]["type"] == "app_home_opened" && params["event"]["tab"] == "home"
    handle_tab_opened(params)
  elsif params["event"]["type"] == "message" && params["event"]["subtype"] != "bot_message"
    handle_message(params)
  end
  status :ok
end

イベントタイプがapp_home_opendでメッセージタブに対するイベントを受け付けると,次に
handle_messages_opend関数が呼び出されます.

ここでは次のような処理が行われています.
1. Slack上にある最新のメッセージのtimestampを取得
2. Twitter APIを用いて取得したtimestampよりも新しいTweetの情報を取得
3. 取得したTweetをもとにブロックを組み立ててUIを構築
4. Slack Web APIを使用してTweetをSlackにpost

app.rb
...
def handle_messages_opened(params)
  twitter_client = TwitterClient.new
  slack_client = SlackClient.new
  channel = params["event"]["channel"]

  latest_timestamp = slack_client.latest_timestamp(channel)
  tweets = twitter_client.home_timeline(latest_timestamp)

  tweets.each do |tweet|
    block = [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: tweet.full_text
        }
      },
      {
        type: "context",
        elements: [
          {
            type: "mrkdwn",
            text: Time.at(tweet.created_at).getlocal.strftime("%-H時%-M分・%Y年%-m月%-d日")
          }
        ]
      },
      {
        type: "actions",
        elements: [
          {
            type: "button",
            text: {
              type: "plain_text",
              emoji: true,
              text: "♡ #{tweet.favorite_count}"
            },
            value: tweet.id.to_s,
            action_id: "favo"
          },
          {
            type: "button",
            text: {
              type: "plain_text",
              emoji: true,
              text: "RT #{tweet.retweet_count}"
            },
            value: tweet.id.to_s,
            action_id: "retweet"
          }
        ]
      }
    ]

    slack_client.post_message(
      channel,
      block,
      "#{tweet.user.name} tweet",
      "#{tweet.user.name} / @#{tweet.user.screen_name}",
      tweet.user.profile_image_uri.to_s
    )
  end
end
...

外部APIとのやり取りには,クライアントライブラリを使用しています.このライブラリをラップする形で,slack_client.rbとtwitter_client.rbの2つのファイルがapp.rbの他に存在します.

3. Tweet

タイムラインを眺めるだけは退屈なので,tweet機能を導入します.
普段Slack上でチャットをするように,メッセージを投稿すると,その内容がTwitterにも投稿されるようにします.message.imイベントを有効にしているため,チャンネル上にメッセージを投稿すると先ほどと同様に指定したURLへリクエストが送られます.params["event"]["subtype"] != "bot_message"を指定しておかないと,タイムラインを更新する際の,botによる投稿にも反応してしまいます.

app.rb
post '/event' do
  params = JSON.parse(request.body.read)
  ...省略...
  elsif params["event"]["type"] == "message" && params["event"]["subtype"] != "bot_message"
    handle_message(params)
  end
  status :ok
end

入力したメッセージが取得できれば,あとはTwitter Apiを用いてPostするだけです.

4. ファボ・リツイート

ボタンがクリックされたら,以下の処理が実行されます.
handle_messages_opened関数の中で,ブロックを用いてUIを組み立てたと思いますが,その時に付与したaction_idをもとに,2つのボタンのどちらが押されたかを識別することができます.

app.rb
post '/interaction' do
  params = JSON.parse(request.params["payload"])
  return if params["token"] != ENV["SLACK_VERIFICATION_TOKEN"]

  if params["actions"][0]["action_id"] == "favo"
    handle_favo_button(params)
  elsif params["actions"][0]["action_id"] == "retweet"
    handle_retweet_button(params)
  end
end

action_idに応じて,favoとretweetの処理に振り分けます.valueでfavoやリツイートの対象となる
ツイートのIDを持たせてあるので,呼び出された関数でtwitter apiを使用して実行します.

まとめ・展望

カレンダー担当日をすっかり忘れていて,急ピッチで実装を行いました.
解説も実装も適当で申し訳なかったです.時間をみて修正を進めて参ります.
加えて,使用できるTwitterの機能もいまだ限定的なので,追加で実装を進めていこうと思います!また,今後OAuthを導入することで,Slack Appを導入したワークスペースに所属する他のユーザについてもTwitterができるような仕組みを実装できればなと考えています.

参考文献

Mercari:GolangでSlack Interactive Messageを使ったBotを書く
Qiita:Slack API 新機能を使ってアプリのホーム・ヴューを活用しよう