【第二回】Rubyで作ったSlack BotとSpotify APIで遊んでみた

この記事はCAMPHOR- Advent Calendar 2016の17日目の記事です

謝罪

いきなり謝罪です。Advent Calendar上で予告していたタイトル「Rubyで作ったSlack BotとRhymerで遊んでみた」ですが、大嘘であることをここに謝罪します。ごめんなさい。

もともとは今年の中頃に一部界隈をおおいに賑わせたRhymerを使って遊ぼうと思っていたのですが、諸般の事情(スベりそう)から止むを得ず内容を変更させていただきました。興味のある方は以下のリンクより Rhymerをお楽しみください↓↓

qiita.com

ごあいさつ

先日の記事は多くの方に読んでいただけたようで非常に嬉しく思います。本日は予告通り2本立ての2本目にあたる内容です。

みなさん、Spotify使ってますか?

Subscription系の音楽サービスとしては近年「Apple Music」「AWA」「LINE Music」など多くのプレイヤーが参入してきましたが、特に洋楽好きの方などはまだまだ「Spotify」人気が根強いのではないでしょうか。今回はみんな大好きSpotifyAPIを使ってSlack Botを強化していきたいと思います。

概要

  • Bot本体は前回の記事で使用したファイルをそのまま使ってます
  • bgmというコマンドを入力することでSlackのタイムライン上にSpotifyの埋め込みリンクを表示するBotを作ります
  • 楽曲、アーティスト、アルバム、プレイリストのそれぞれで検索機能を実装
  • ジャンルをもとにしたレコメンド機能を実装
  • より複雑な機能を実装しようとおもったけど断念(後述)
  • 正規表現の理解度がダメダメなのでツッコミお待ちしています

目次

  1. RSpotify
  2. 検索機能の実装
  3. レコメンド機能の実装
  4. 個人利用のためのカスタマイズ(断念)

環境

  • macOS Sierra 10.12.2 Beta
  • MacBook Air (13-inch, Early 2014)
  • ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15]

参考

やっていくぞ!!

1. RSpotify

RSpotifyGuilherme Sad氏によるSpotify Web APIRubyラッパーです

1.1. 開発用キーの設定

Spotify Developer Pageから新規アプリケーションを作成し、 Client IDClient Secretの2つのトークンを取得します。

f:id:andoshin11:20161216070927p:plain

f:id:andoshin11:20161216071103p:plain

トークンを取得したらそれぞれ SPOTIFY_CLIENT_IDおよび SPOTIFY_CLIENT_SECRETという環境変数として保存します。方法は前回の記事を参考にしてください。

1.2. RSpotifyのインストール

前回作成した Gemfileに追記

# Gemfile
source 'https://rubygems.org'
ruby "2.3.1"

gem 'http'
gem 'json'
gem 'faye-websocket'
gem 'eventmachine'
gem 'rspotify'

$ bundleコマンドでGemをインストール f:id:andoshin11:20161216051148p:plain

1.3. RSpotifyの動作確認

RSpotifyと関連するGemのインストールが完了したら動作をテストしてみましょう。前回同様、 bot.rbに追記していきます。

# bot.rb
require 'http'
require 'json'
require 'eventmachine'
require 'faye/websocket'
require 'rspotify' # RSpotifyの読み込み

# アプリの認証情報
RSpotify.authenticate(ENV['SPOTIFY_CLIENT_ID'], ENV['SPOTIFY_CLIENT_SECRET'])

(省略)

  ws.on :message do |event|
    data = JSON.parse(event.data)

    # data['text']が存在しなければスキップし、存在すればmsg変数に代入
    next if data['text'].nil?
    msg = data['text']

    p [:message, data]

    if msg == 'こんばんは'
      ws.send({
        type: 'message',
        text: "こんばんは <@#{data['user']}> さん"+data['text'].split(" ").first,
        channel: data['channel']
        }.to_json)
    end

    # "bgm"という単語から始まるテキストが入力された時の処理
    if msg.match(/^bgm/)
      test_url = RSpotify::Track.find('5CwhBU7eqQgkOVMnRNuzzT').external_urls['spotify']
      ws.send({
        type: 'message',
        text: test_url,
        channel: data['channel']
        }.to_json)
    end
  end

(省略)

bgmという単語から始まるテキストがSlackに投稿された時に Spotifyから 5CwhBU7eqQgkOVMnRNuzzTというIDに該当する曲を探し、その埋め込み用リンクを返すような処理です。

このコードを実行してみましょう。 f:id:andoshin11:20161216053008p:plain

https://gyazo.com/d978f4f1449f7ab0e7f33db822a1ab1f.gif

ID: 5CwhBU7eqQgkOVMnRNuzzTの曲、つまりGet Wildの埋め込みリンクが無事タイムラインに投稿されました!

2. 検索機能

2.1. 楽曲を検索できるようにする

無限にGet Wildを聴き続けるのもよいですが、せっかくなら楽曲を検索して表示したいものです。

bgm song (クエリ)というテキストが入力されたらクエリに該当する楽曲を検索

# bot.rb
...

    if msg.match(/^bgm/)
      text = "No result"
      if matched_msg = msg.match(/^bgm song (.*)/)
        track = RSpotify::Track.search(matched_msg[1]).first
        text = track.external_urls['spotify'] unless track.nil?
      end
      ws.send({
        type: 'message',
        text: text,
        channel: data['channel']
        }.to_json)
    end
...

実行

https://gyazo.com/4bbcd364454951083817bb7f1f97e580.gif

2.2 アーティスト、アルバム、プレイリストの検索機能

上記のコードと同じ要領でアーティストやアルバム、プレイリストについても検索機能を実装していきます

# bot.rb

...
    if msg.match(/^bgm/)
      text = "No result"
      if matched_msg = msg.match(/^bgm song (.*)/)
        track = RSpotify::Track.search(matched_msg[1]).first
        text = track.external_urls['spotify'] unless track.nil?
      elsif matched_msg = msg.match(/^bgm artist (.*)/)
        artist = RSpotify::Artist.search(matched_msg[1]).first
        text = artist.external_urls['spotify'] unless artist.nil?
      elsif matched_msg = msg.match(/^bgm album (.*)/)
        album = RSpotify::Album.search(matched_msg[1]).first
        text = album.external_urls['spotify'] unless album.nil?
      elsif matched_msg = msg.match(/^bgm playlist (.*)/)
        playlist = RSpotify::Playlist.search(matched_msg[1]).first
        text = playlist.external_urls['spotify'] unless playlist.nil?
      end
      ws.send({
        type: 'message',
        text: text,
        channel: data['channel']
        }.to_json)
    end
...

実行!

https://gyazo.com/1f7ad569f004bab3d9c34ca81085536f.gif

https://gyazo.com/c46fa903e6348523f8463957e55ad0a2.gif

YOSAGE(ガッツポーズの絵文字)

3. レコメンド機能

音楽のジャンルごとに楽曲をレコメンドしてくれる機能を実装します。 bot.rbに追記

# bot.rb
...

    if msg.match(/^bgm/)
      text = "No result"
      if matched_msg = msg.match(/^bgm song (.*)/)
        track = RSpotify::Track.search(matched_msg[1]).first
        text = track.external_urls['spotify'] unless track.nil?
      elsif matched_msg = msg.match(/^bgm artist (.*)/)
        artist = RSpotify::Artist.search(matched_msg[1]).first
        text = artist.external_urls['spotify'] unless artist.nil?
      elsif matched_msg = msg.match(/^bgm album (.*)/)
        album = RSpotify::Album.search(matched_msg[1]).first
        text = album.external_urls['spotify'] unless album.nil?
      elsif matched_msg = msg.match(/^bgm playlist (.*)/)
        playlist = RSpotify::Playlist.search(matched_msg[1]).first
        text = playlist.external_urls['spotify'] unless playlist.nil?

      # 追記部分
      elsif matched_msg = msg.match(/^bgm recommend (.*)/)
        result = RSpotify::Recommendations.generate(seed_genres: [matched_msg[1]]).tracks.sample
        text = result.external_urls['spotify'] unless result.nil?
      end

      ws.send({
        type: 'message',
        text: text,
        channel: data['channel']
        }.to_json)
    end
...

bot recommendから始まる入力が与えられたときに RSpotify::Recommendations.generateを実行しています。 RSpotify::Recommendations.generateからはおすすめの track20曲分(デフォルト時)の配列が得られるため、その中からランダムで1曲を抜き出して埋め込みリンクを表示するような処理です。

レコメンド機能を試してみましょう。

https://gyazo.com/9f6a6eb6f3641eb62b28b76173c9fa8d.gif

こちらもYOSAGEですね。ちなみに2016年12月16日現在レコメンド機能がカバーしているジャンルは以下の通りです

  {
  "genres" : [ "acoustic", "afrobeat", "alt-rock", "alternative", "ambient", "anime", "black-metal", "bluegrass", "blues", "bossanova", "brazil", "breakbeat", "british", "cantopop", "chicago-house", "children", "chill", "classical", "club", "comedy", "country", "dance", "dancehall", "death-metal", "deep-house", "detroit-techno", "disco", "disney", "drum-and-bass", "dub", "dubstep", "edm", "electro", "electronic", "emo", "folk", "forro", "french", "funk", "garage", "german", "gospel", "goth", "grindcore", "groove", "grunge", "guitar", "happy", "hard-rock", "hardcore", "hardstyle", "heavy-metal", "hip-hop", "holidays", "honky-tonk", "house", "idm", "indian", "indie", "indie-pop", "industrial", "iranian", "j-dance", "j-idol", "j-pop", "j-rock", "jazz", "k-pop", "kids", "latin", "latino", "malay", "mandopop", "metal", "metal-misc", "metalcore", "minimal-techno", "movies", "mpb", "new-age", "new-release", "opera", "pagode", "party", "philippines-opm", "piano", "pop", "pop-film", "post-dubstep", "power-pop", "progressive-house", "psych-rock", "punk", "punk-rock", "r-n-b", "rainy-day", "reggae", "reggaeton", "road-trip", "rock", "rock-n-roll", "rockabilly", "romance", "sad", "salsa", "samba", "sertanejo", "show-tunes", "singer-songwriter", "ska", "sleep", "songwriter", "soul", "soundtracks", "spanish", "study", "summer", "swedish", "synth-pop", "tango", "techno", "trance", "trip-hop", "turkish", "work-out", "world-music" ]
}

4. 個人利用のためのカスタマイズ(...だがしかし断念)

当初の予定ではここからユーザー情報で OAuth認証してSlackから個人のプレイリストに曲追加したりいろいろやりたいことがあったけど、Advent Calendar担当日のタイムリミットが来たので断念...

近いうちにリベンジしたいです。

おわりに

日本でのサービスイン以前からSpotifyを利用しているヘビーユーザーなのでAPIを叩くだけで遊べるのはとてもありがたいです。 Slack Botに限らず今後もいろんな機会で使っていきたいと思います。

明日はmurata_atsumiの記事です

Let's Get Wild!

open.spotify.com

【第一回】超簡単!RubyでSlack Botを作る方法

この記事はCAMPHOR- Advent Calendar 2016の13日目の記事です

ごあいさつ

今年もAdvent Calendarの季節がやってきましたね。 せっかくなのでこれを機にTech関連の記事を残すため、はてなブログはじめました。 相変わらず見習いエンジニアの域を出ない@andoshin11 です。 今回の記事は2本立ての構成になっています。(2本目の記事は12/17に公開予定)

またまた長文になりますがお付き合いよろしくお願いします。

昨年度の記事はこちら↓↓

tech.camph.net

概要

  • プログラミング初心者でも記事を読めばBotが作れるよう意識して書きました*1
  • 昨年は Googla Apps Scriptを使用しましたが、今年は RubyでSlack Botを実装します
  • Slack公式が提供する Real Time Messaging APIを利用します
  • Botを作成し、外部サーバー(Heroku)で動かすところまでを目標にします
  • rubotylitaのようなライブラリは利用せず、勉強も兼ねて自前での実装を目指します
  • 質問あればコメントください!

目次

  1. Slack API入門
  2. Real Time Messaging API
  3. Herokuにデプロイするぞ

環境

参考

やっていくぞ!!

1. Slack API 入門

幸いなことにSlack自身がすでに豊富なAPIを整えてくれているので、その使い方さえ理解すれば比較的容易に手元のマシンからでもメッセージの送受信ができます。

Slack API

1.1. APIのテスト

何はともあれ RubySlack APIを叩いて遊んでみましょう! まずは作業ディレクトリ内に Gemfileを作成します。

# Gemfile
source 'https://rubygems.org'

gem 'http'
gem 'json'

次に本体である test.rbの作成です

# test.rb
require 'http'
require 'json'

response = HTTP.post("https://slack.com/api/api.test")
puts JSON.pretty_generate(JSON.parse(response.body))

2つのファイルの用意ができたら $ bundle exec ruby test.rbスクリプトを実行します。

f:id:andoshin11:20161209230148p:plain

POSTしたリクエストへのレスポンスとして "ok":trueが返ってきましたね。Slack本体のサーバーと対話ができた証拠です。

1.2. Authentication

次はもう一歩踏み込んでチーム固有の情報を取得できるよう、 Authenticationを行なっていきたいと思います。

先ほどはhttps://slack.com/api/api.testというURLにリクエストを送りましたが今度はhttps://slack.com/api/auth.testというアドレスにリクエストを送ってみます。

test.rbを編集

# test.rb
require 'http'
require 'json'

response = HTTP.post("https://slack.com/api/auth.test")
puts JSON.pretty_generate(JSON.parse(response.body))

実行

f:id:andoshin11:20161209230259p:plain

"ok":false, "error": "not_authed"と怒られてしまいました。これはリクエスト時に Tokenが設定されていないことが原因です。 Tokenは、管理画面から Bots Integrationを追加することで取得できます。 f:id:andoshin11:20161209190042p:plain f:id:andoshin11:20161209190056p:plain

発行された Tokenをコピーして環境変数として保存しましょう。

f:id:andoshin11:20161209230425p:plain

POSTリクエストに params[:token]を追加。

require 'http'
require 'json'

response = HTTP.post("https://slack.com/api/auth.test", params: {
    token: ENV['SLACK_API_TOKEN']
  })
puts JSON.pretty_generate(JSON.parse(response.body))

実行

f:id:andoshin11:20161211222749p:plain

成功しました!今度は認証が認められ、加えて Tokenに紐づいたチームの情報やユーザーの情報も返ってきています。 余談ですがSlack内で割り振られている team_idは先頭が Tから始まり、 user_idUから始まるという仕様のようです。

1.3. Post Message

認証が通るようになったので、実際にSlackへメッセージを投稿してみましょう。

メッセージ投稿用APIのURLはhttps://slack.com/api/chat.postMessageです。パラメータに投稿先のチャンネルや、投稿したいテキストを指定してリクエストを送ることでメッセージの投稿が可能になります。

test.rbを編集

require 'http'
require 'json'

response = HTTP.post("https://slack.com/api/chat.postMessage", params: {
    token: ENV['SLACK_API_TOKEN'],
    channel: "#general",
    text: "こんにちは!",
    as_user: true,
  })
puts JSON.pretty_generate(JSON.parse(response.body))

as_user: trueとしているのはデフォルトの Bots Integrationとしてではなく、自分で ConfigureしたBotユーザーに投稿させたいからです。

スクリプトを実行

f:id:andoshin11:20161211223037p:plain

f:id:andoshin11:20161211223050p:plain

メッセージの投稿に成功しました!このときレスポンス内に投稿されたメッセージの関する情報が含まれているのもわかりますね(タイムスタンプ等)

ここまでで基本的な Slack APIの使い方を把握できました! Cron等で定期的にこのメッセージ投稿APIを叩くだけでも簡易Botが作れそうですね。

2. Real Time Messaging API

前節ではあくまで一方通行のメッセージの投稿方法を確認しました。 ですが実際の運用を想定すると常時双方向通信を監視し、ユーザーからの投稿に合わせて適切な出力を返してあげる必要があるでしょう。 ここからはSlackの備える Real Time Messaging APIと低コストで双方向通信を可能にする Web Socketの仕組みを利用して、より実用的なBotを作っていきます。

2.1. Real Time Messaging APIとは

Slackには既に Outgoing Webhookというユーザーの投稿内容を外部のサイトやアプリに出力してくれる仕組みがありますが、 Outgoing Webhookの扱える情報は特定のキーワードから始まるメッセージのみです。

対して Real Time Messaging APIはその名の通り24時間チームを監視しユーザーの全ての投稿内容だけでなく、「チャンネルの新規作成」「チャンネルへのユーザーの参加/脱退」などといった1つ1つのイベントの情報すら出力してくれる仕組みです。

この仕組みを用いることでより複雑なBotの実装が可能になります。

2.2. Real Time Messaging APIでデータを取得する

まずは Gemfileを編集して2つの新たなGemを追加します

# Gemfile
source 'https://rubygems.org'

gem 'http'
gem 'json'
gem 'faye-websocket'
gem 'eventmachine'

faye-websocketによってRubyでWeb Socketサーバーを立てることが容易になり、 eventmachineは並列処理等を可能にしてくれるものです

早速、https://slack.com/api/rtm.startにリクエストを送り Web SocketのURLを確認しましょう。

# test.rb
require 'http'
require 'json'
require 'eventmachine'
require 'faye/websocket'

response = HTTP.post("https://slack.com/api/rtm.start", params: {
    token: ENV['SLACK_API_TOKEN']
  })

rc = JSON.parse(response.body)

puts rc['url']

実行

f:id:andoshin11:20161209232226p:plain

レスポンスの JSONの中の['url']にアドレスが含まれているのが分かりますね。このアドレスに対して双方向通信を確立するよう、 Web Socketを動かしていきます。 Real Time Messaging APIからは常に情報が降ってくるので、並列処理のための Event Machineもお忘れなく。

# test.rb
require 'http'
require 'json'
require 'eventmachine'
require 'faye/websocket'

response = HTTP.post("https://slack.com/api/rtm.start", params: {
    token: ENV['SLACK_API_TOKEN']
  })

rc = JSON.parse(response.body)

url = rc['url']

EM.run do
  # Web Socketインスタンスの立ち上げ
  ws = Faye::WebSocket::Client.new(url)

  #  接続が確立した時の処理
  ws.on :open do
    p [:open]
  end

  # RTM APIから情報を受け取った時の処理
  ws.on :message do |event|
    p [:message, JSON.parse(event.data)] 
  end

  # 接続が切断した時の処理
  ws.on :close do
    p [:close, event.code]
    ws = nil
    EM.stop
  end

end

実行!

f:id:andoshin11:20161209232530p:plain

一番上に接続が確立したことを示す [:open]が表示されていますね。

二行目の [:message]に含まれる {"type"=>"hello"}というのはSlack側が接続確立時に送信してくるものです。三行目は無視して構いません。

四行目の内容ですが {"type"=>"presence_change"...}というメッセージからも分かる通り、ユーザーのステータスが変わったことを示すイベントをキャッチしてReal Timeでこちらの Web Socket Serverに送信してくれています。

このプログラムを走らせたまま、もう少しSlack側でいろいろいじってみましょう。

https://gyazo.com/7c40944aa3c8d36adccac896dd444e5b.gif

https://gyazo.com/059890bb5da0f256b26665cbae0901de.gif

みなさんお気付きのように Real Time Messaging APiではユーザーが投稿する時だけでなく、「入力中」であることや「リアクション」を取った事すらも細かく取得することがわかりました。これで様々な種類のBotが実装できますね!

2.3 ユーザーの投稿に合わせてメッセージを返す

ここまでで常時ユーザーの投稿内容が取得できるようになりました。 次は投稿に合わせたレスポンスを返せるよう test.rbを修正していきます。

# test.rb
...

  ws.on :open do
    p [:open]
  end

  ws.on :message do |event|
    data = JSON.parse(event.data)
    p [:message, data]

    if data['text'] == 'こんにちは'
      ws.send({
        type: 'message',
        text: "こんにちは <@#{data['user']}> さん",
        channel: data['channel']
        }.to_json)
    end
  end

  ws.on :close do
    p [:close, event.code]
    ws = nil
    EM.stop
  end

...

ユーザーが「こんにちは」と投稿したら、「こんにちは (ユーザー名)さん」と同じチャンネルで返すようなシンプルなスクリプトです。

実行!

https://gyazo.com/4eedd5ba19eacd9def0f7ee0ccd8e4dc.gif

意図した通りに動きましたね。

1つ注意しなくてはいけないのが、Botが投稿した内容もまた RTM APIによって返ってくるということです。Bot同士の会話が一生終わらないという事態が無いように気をつけてください。 f:id:andoshin11:20161209202001j:plain

RTM APIで受信したJSON内に含まれる情報を上手く利用することで双方向対話ができるようになりました。おめでとうございます!

3. 作ったBotを公開する

ここまででLoaclのマシン上でBotが動くようになりましたが、せっかくなので外部サーバーでホスティングして常時稼働させてあげたいものです。

今回は Herokuにデプロイすることでこの課題を解決していきます。 事前準備として heroku toolbeltを導入するところまでは進めておいてください。

参考リンク:Heroku登録〜Macで環境整備〜お試しWebアプリを作るまで

3.1. Gitの設定

Heroku上で動かすにあたって GemfileRubyのバージョンに関する情報を追記します。

# Gemfile
source 'https://rubygems.org'
ruby "2.3.1"

gem 'http'
gem 'json'
gem 'faye-websocket'
gem 'eventmachine'

また test.rbの名前をカッコ悪いので bot.rbに変更し、「こんばんは」というコマンドに反応するよう修正しました。

# bot.rb
...

  ws.on :message do |event|
    data = JSON.parse(event.data)
    p [:message, data]

    if data['text'] == 'こんばんは'
      ws.send({
        type: 'message',
        text: "こんばんは <@#{data['user']}> さん",
        channel: data['channel']
        }.to_json)
    end
  end

...

お決まりの $ git init $ git add . $ git commit -m "Initial commit"までの流れで Gitの初期化を行います

f:id:andoshin11:20161210011218p:plain

3.2. Heroku側の設定

Heroku のアカウントを取得し Heroku Toolbeltの設定も済んでいる方はコマンドラインからログインができます。

$ heroku login実行後ログイン情報を入力

f:id:andoshin11:20161210004318p:plain

初期状態では Heroku上に Rubyの実行環境が存在しないため、公式の提供する Heroku Buildpack for Rubyを利用してアプリケーションの初期化を行います。

$ heroku create --buildpack https://github.com/heroku/heroku-buildpack-ruby.gitを実行

f:id:andoshin11:20161210004746p:plain

$ git push heroku masterでファイルをデプロイ!

f:id:andoshin11:20161210011545p:plain

TokenHeroku環境変数として登録します

f:id:andoshin11:20161210011706p:plain

さぁ、ここまでで準備完了です。実際に Heroku上で Rubyスクリプトを動かしてみましょう!

$ heroku run bundle exec ruby bot.rbを実行

f:id:andoshin11:20161210012017p:plain

https://gyazo.com/5abef68450c61f6c09c56f31323952c7.gif

動いたー!!

おわりに(...そして予告)

お疲れ様でした。Slackの強力なAPIWeb Socketの力でシンプルなスクリプトでもBotを作れるようになりましたね。やっている事は本当に単純なのでどんな言語でも応用が効くことでしょう。

冒頭でもお伝えしましたが今回は2本立てです。 次回の内容は、今回作成したプログラムにアップデートを加えてより実用的(????)なBotを作る手段を記事にします。 お楽しみに!

明日はsiriusjackの記事です

*1:僕自身が雑魚エンジニアなので...

*2:MBAディスコンになってしまったのでかなしい...