Slack Botの土台をGASで構築

Slack Botの土台をGASで構築

業務効率を上げたり, 時には癒しをくれたり, 調べてみると様々なBotの活用事例があるようです. この記事ではSlackBotの土台となるものをGASで構築していきます.

ゴール

この記事を読んだらなにができるようになるかを述べておきましょう. 本記事におけるBotの土台とは,

Slackで特定のワードを発言すると, Botが反応して一定の返事をくれる

というものです. これだけっちゃこれだけですが, GASを利用しているので, 以下のようなBotに発展させることもできるでしょう.

手順

以下のような手順で進めていきます.

  • Webhook URLを取得
  • SlackからPOSTリクエストを受け取り, POSTリクエストを返す機構をGASで構築
  • SlackからPOSTリクエストを発行するためのトリガーを設定

Webhook URLを取得

Webhook URLというのは, メールアドレスのようなものですね. このURLに対しPOSTリクエストを投げることで, Slackにメッセージを投稿することができます. ではやっていきましょーう.

まずはSlack APIにアクセスしましょう. 右上のCreate New Appをクリックします. そうなんです実は, これからBotをSlackアプリとして作ろうとしているんですね. さてCreate New Appをクリックすると次のようなダイアログが出現します.

f:id:riemann1618:20180928204913p:plain

上の欄には好きなアプリ名を, 下の欄では実験用のWorkspaceを選択しましょう. 注意書きにあるように, アプリ名は後から変えられるので心配いりません.

入力を終えたらCreate Appをクリックします.

すると以下のようなページに飛びます.

f:id:riemann1618:20180928205106p:plain

まずはBotsをクリックし, このアプリがBotとして動作するよう設定します. といっても2つの入力欄を埋め, Always Show My Bot as Onlineをオンにするだけです.

f:id:riemann1618:20180928205817p:plain

この設定を保存したら, 次に左のメニューのIncoming Webhooksをクリックし, トグルをONにします.

f:id:riemann1618:20180928210003p:plain

下の方にいってAdd New Webhook to Workspaceをクリック.

f:id:riemann1618:20180928210058p:plain

Botからの返信を待ち受けるチャンネルをここで選択します. 選択できたらAuthorizeをクリック.

f:id:riemann1618:20180928210439p:plain

するとSuccess!というFlashメッセージとともに, 先のページにリダイレクトされます. 下のほうをみると, Webhookが追加されているのがわかりますね. Webhook URLもちゃんと書いてあります. これで第一段階は終了です.

f:id:riemann1618:20180928210758p:plain

SlackからPOSTリクエストを受け取り, POSTリクエストを返す機構をGASで構築

さて次はGASです. Google ドライブ にアクセスし, GASプロジェクトを新規作成します. プロジェクト名はお好きに決めてください.

f:id:riemann1618:20180928211112p:plain

ここからはコーディングです. GASのベースはJavaScriptですから, メソッド名の最後に()をつけたり, 式をセミコロンで締めくくったり, 気をつけなければならないことがたくさんありますね...

// POSTリクエストを受け取る関数
function doPost(e) {
  
  // Webhook URLを変数に格納
  var url = 'https://hooks.slack.com/services/ほにゃらら';
  
  // メッセージ本文
  var payload = {
    'text': 'ここが本文'
  };

  // HTTPリクエストの設定
  var options = {
    'method': 'post',
    'contentType': 'application/json',
    'payload': JSON.stringify(payload)
  }
  
  // SlackへのPOSTリクエストを発行
  UrlFetchApp.fetch(url,options);
}

各コードの意味はコメントに書いたので, だいたい理解できるかと思います.

コードが書けたら上の方にあるデバッグボタンを押して, 問題がないかチェックします. 問題がなければコードが実行されるので, Slackチャンネルに「ここが本文」というメッセージが投稿されるはずですが, どうでしょうか...?

次はこのアプリをネット上に公開する必要があります. そうでないとSlackからPOSTリクエストを投げられないですからね. 上の方の「公開」から, 「アプリケーションとして導入」を選択します.

f:id:riemann1618:20180928211535p:plain

アプリケーションにアクセスできるユーザーを, 「全員(匿名ユーザーを含む)」に設定し, 「導入」をクリックします. 「変更内容の説明」は空欄でOKです.

f:id:riemann1618:20180928211553p:plain

すると承認が必要という旨のダイアログが出るので, 誘導にしたがって承認作業をしてください.

次のようなダイアログが出れば成功です. 「現在のウェブ アプリケーションのURL」はあとで使うので, ダイアログは出したままでOKです.

f:id:riemann1618:20180928211923p:plain

これで第二段階は終了です.

SlackからPOSTリクエストを発行するためのトリガーを設定

次は, SlackにTrigger wordを設定します. まず実験用に用意したWorkspaceへアクセスしてください. 左のペインからアプリを検索します.

f:id:riemann1618:20180928212222p:plain

Outgoing WebHooksを検索します. 数文字入力すれば十分でしょう. インストールボタンをクリックします.

f:id:riemann1618:20180928212307p:plain

Add Configurationをクリックします.

f:id:riemann1618:20180928212642p:plain

次のページでAdd Outgoing WebHooks integrationをクリックします.

リダイレクト先のページを下にスクロールすると, 以下のような入力フィールドが現れます.

f:id:riemann1618:20180928212911p:plain

ChannelではTrigger wordに反応してほしいチャンネルを選択します.

Trigger wordsはコンマ区切りで入力しましょう.

URLにはさっきの「現在のウェブ アプリケーションのURL」を入力します.

以上の入力ができたら一番したまでスクロールし, Save Settingsをクリックします.

これで第三段階も完了です.

完成とその後

これで全ての手順が終了しました. Slack上でTrigger wordを送信すると, 自動で返信が来ることが確認できるかと思います. 返信の表示形式をもっとリッチにする方法を, またこんど記事にするつもりです.

そして上でも述べましたが, GoogleカレンダーGoogle翻訳, Gmailなどのサービスを活かしたBotを作成することもできます. 本格的なBotを作るのは無理でも, 多少遊んでみるくらいはできそうです.

【Rails5】数時間溶かしたエラーの原因はRoutingにあった

長時間悩んだ末にエラーが解決できると, なんとも言えない達成感があります. それと同時に,「俺の時間があああ」とも思いますが...

今回時間を奪ったものは正確に言うと, エラーではなくテストです. 何度やってもテストが通りませんでした.

しかしエラーも引き起こし得るミスですから気をつけたいところですね. ただしRESTfulにRoutingを組んでおけば起ることのない事態ですから, もっとRESTを意識しておけばよかったのかもしれません.

間違ったコード

テストが通らなかった状態では, 以下のような記述をしていました.

# routes.rb
get :account_activation, to: 'account_activations#activate'
# account_activations_controller.rb
def activate
  # params[:id] を使う処理
end

なにがまずいのか

上のRoutingではnamed pathの挙動が以下のようになります.

irb(main)> app.account_activation('IdForActivation')
=> "/account_activation.IdForActivation"

本当はaccount_activation/IdForActivationというパスになって欲しいのに, そうはなっていないんですね.

正しいコード

:idを使うことをRailsに教えてやればOKです.

# routes.rb
get 'account_activation/:id', to: 'account_activations#activate', as: :account_activation

asオプションは, named pathを設定するためのものです. これがないとnamed pathが使えませんでした.

補足とまとめ

はじめにも述べたように, resourcesresourceメソッドを用いてRESTfulにRoutingを組んでいれば起こらないミスでした. もしRESTfulにするならば,

resources :account_activations, only: [:edit]

のように書けばよかったですね. もちろんこの場合はアクション名もactivateからeditに変わりますが.

今回得た学びは, 便利メソッドがやってくれていることを理解しよう ということですね.

resourcesというメソッドは7つものRoutingを一気に設定してくれます. しかしカスタマイズされたRoutingを設定したい場合は, resourcesの仕事を肩代わりしなくてはなりません. そのためにはその仕事内容を理解する必要があるということでした.

【Rails5】Herokuへのデプロイ

基本的に【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】を参考にさせてもらった. でもやっぱりね, エラーは出るもんですよ.

赤文字で出たエラーがこれ.

Precompiling assets failed.

だからこればっかり検索してたんだけど, あまりいい情報が出てこなかった. それもそのはず. もっと手前で問題が発生していた.

Precompiling assetsfailしたのは, 単なる結果だったというわけ. もっとログを読めばよかったね. ログ全体を見て気になった点は以下の文言.

  • Warning: Multiple default buildpacks reported the ability to handle this app. The first buildpack in the list below will be used.

  • Warning: the running version of Bundler (1.15.2) is older than the version that created the lockfile (1.16.4).

  • Uglifier::Error: Unexpected token name «of», expected punc «;». To use ES6 syntax, harmony mode must be enabled with Uglifier.new(:harmony => true).

結論から言うと, 気にすべきなのは最後のエラーだけ. このエラーに関しては対処法が見つかった. config/environments/production.rbを以下のように編集.

# config/environments/production.rb
-  config.assets.js_compressor = :uglifier
+  config.assets.js_compressor = Uglifier.new(harmony: true)

これでなんとかデプロイできたああ.

【Rails5】Autoprefixer doesn’t support Node v4.8.2. というエラーへの対処法

Rails5での開発中に

Autoprefixer doesn’t support Node v4.8.2.

というエラーに遭遇しました. (少なくとも私の環境では解決した)対処法をまとめました.

環境

対処法

検索してみると, Stack Overflowで質問されているのが見つかりました.

node.js - Ruby on Rails - Autoprefixer doesn’t support Node v4.9.1. Update it. How to fix? - Stack Overflow

この解答の通り, mini_racerというGemを追加したら直りました.

結局どんなエラーだったの?

経験が浅いなりに調べてみました. 結局どうしてmini_racerを導入するとエラーが解消されたかははっきりわかっていません. しかし原因を調べていく上で自分にとって新しい知識が得られたのでまとめておきます. エラーが解消された理由を推測することはできるかもしれません.

Vender Prefixes とは

CSSにはVender Prefixesというものがありまして, これについては以下のブログにとてもわかりやすく書かれていました.

まだベンダープレフィックスで消耗してるの? – ysklog

少し引用しておきましょう.

「ベンダープレフィックス」とは、主に、CSS3で採用予定(草案段階)の機能を各ブラウザが先行的に実装し、その先行実装した機能を有効化させるために必要な各ブラウザ毎の「-moz-」や「-webkit-」という接頭辞のことをいいます。

つまり, 機能によってはブラウザごとに書かなくてはならない面倒なもの, ということですね.

Autoprefixerとは

面倒なVender Prefixesを自動でつけてくれるのが, AutoprefixerというGemです. エラー文にも登場していますね.

ExecJSとは

ExecJSというGemにより, JavaScriptで書かれたコードをRubyから実行することができるようになります. ExecJSがサポートしているruntimeとして, 以下などが挙げられています.

  • therubyracer
  • Node.js
  • mini_racer

mini_racerとは

これはGitHubのReadmeを読んでもらうのが良いかと. GitHub - discourse/mini_racer: Minimal embedded v8

こちらも少し抜粋しておきましょう.

MiniRacer provides a minimal two way bridge between the V8 JavaScript engine and Ruby.

It was created as an alternative to the excellent therubyracer. Unlike therubyracer, mini_racer only implements a minimal bridge. This reduces the surface area making upgrading v8 much simpler and exhaustive testing simpler.

JavaScriptエンジンのV8とRubyの橋渡しをするものだそうですね. ExecJSはmini_racerのようなものを使って, RubyからJavaScriptを実行するということなのでしょう.

エラーが解消された理由

すでに述べましたが, 正直よくわかっていません.

mini_racerを導入することで, ExecJSはNode.jsではなくmini_racerを使うようになった, ということでしょうか.

【Rails5】`find`と`find_by`の違い

単一のレコードを取得する2つのメソッドの違いをまとめました. これについての記事はいくらでもあるけどね.

違い

find

  • idで検索を行う.
  • レコードが見つからない場合はエラー.
User.find(1000)
ActiveRecord::RecordNotFound (Couldn't find User with 'id'=10)
  • nilを渡した場合も.
User.find(nil)
ActiveRecord::RecordNotFound (Couldn't find User without an ID)

find_by

  • id以外でも検索可能.
User.find_by(email: 'hello@world.com')
  • レコードが見つからない場合はnilを返す.
User.find_by(id: 1000) #=> nil
  • nilを渡した場合はIS NULLで検索.
User.find_by(id: nil)
User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1                                                        

使い分け

404エラーを表示させたい場合はfind

例えばユーザーの詳細ページ(プロフィールページ)を考えてみましょう. コントローラでは以下のようにインスタンス変数を定義するでしょう.

def show
  @user = User.find(params[:id])
end

もしexample.com/users/1000のように, 存在しないユーザーを表示させようとすると, 404エラーページを表示してくれます.

ここでfind_byを使ってしまうと, @user = nilとなり, view側でエラーが発生してしまいますね.

nilを期待する場合はfind_by

エラーを発生させたくない, そしてレコードがなければnilを期待するという場合はfind_byを使います. 例えばRails Tutorialで定義されているcurrent_userヘルパーメソッドの定義が以下です(ただし8章時点でのもの).

def current_user
  @current_user ||= User.find_by(session[:user_id])
end

ここでfindを使うと, エラーが生じてしまい大変ですね.

id以外で検索したいが, エラーを発生させたい場合

この場合はfind_by!が用意されています. この記事を投稿して2日後に, ちょうどこのメソッドを使う機会があったので追記します.

例としてPostという架空のモデルを考えます. Postモデルはdraftというboolean型のattributeを持っているとします.

# schema.rb の省略版
create_table "post" do |t|
  t.string "title"
  t.text "body"
  t.boolean "draft"
end

draft: trueによって下書きとしてPostを保存できます. 他人は下書きにアクセスできないようにしたいとします. こんなときは次のように実装すればよさそうです.

class PostsController < ApplicationController
  def show
    @post = Post.find_by!(id: params[:id], draft: false)
  end
end

下書き保存されたPostにアクセスすると, まるでレコードが存在しないかのように振舞ってくれますね.

【Rails】データベース上のデータをシンボルで取得

前置き

私はRails Tutorialを1周しただけの初心者ですから, いろいろツッコミどころがあるかもしれません. ご容赦くださいませ.

やりたいこと

たとえばボクサーのデータベースシステムを作るとします(ボクシング全然知りませんが...). データベースには名前(name)と階級(weight)を保存するものとします. テーブル例は以下の通りです.

id name weight
1 Muhammad Ali heavy
2 Shinsuke Yamanaka bantam

この場合, 取得される階級は文字列型ですね.

(irb) boxer = Boxer.first
(irb) boxer.weight #=> 'heavy'

しかしアプリケーション内において, 階級はシンボルで扱いたいとしましょう. つまり以下のような動作を実現させたいわけです.

(irb) boxer.weight #=> :heavy

実装

答えはわりと簡単です. アクセサメソッドを上書きすればいいのです.

class Boxer < ApplicationRecord

  def weight
    read_attribute(:weight).to_sym
  end
end

実はモデルにはread_attributeというメソッドが用意されています. これをつかってattributeを取得し, シンボルに変換しています.

最後に

そもそも階級と自然数を対応付けて, データベースには自然数を保存すればいいのかもしれませんね. 独自にモジュールやクラスを定義すれば, 上でやりたいことは実現できます.

さらにボクシングの階級は全順序集合ですから, 自然数との対応付けがより意味をなします. つまり階級順に取得したいときは, 自然数の自然な順序を利用すればいいということです.

なんだかまとまらない記事でお粗末様でございました.

has_manyで関連づけられたモデルも一括保存する方法

Nested Attributesによって, has_manyで関連づけられたモデルのデータも丸ごと保存する方法を紹介します.

モデル例

本記事では例として以下のようなモデルを扱うことにします.

class User < ApplicationRecord
  has_many :posts
end

class Post < ApplicationRecord
  belongs_to :user
end

実現したいこと

やりたいことは, UserといくつかのPostをフォームから同時に登録することです. もう少しだけ正確に言うと, Userの登録時に, いくつかのPostもまとめて登録できるようにします. つまりメインのデータはUserということです.

では実装すべきコードを順に見ていきましょう.

モデルの実装コード

以下のようにUserモデルに1行追加します.

class User < ApplicationRecord
  has_many :posts
  accepts_nested_attributes_for :posts
end

これで, Postも一緒に保存するための受け入れ体制ができました.

またPostモデルからは, (もし書いていたら)以下のバリデーション を取り除きます.

class Post < ApplicationRecord
  belongs_to :user
  # validates :user_id, presence: true
end

当然湧いてくるであろう疑問

  • なぜこのバリデーションを除く必要があるのか
  • バリデーションはどのように定義すればいいのか

については最後に説明しますのでご心配なく.

コントローラの実装コード

次はコントローラです.

class UsersController < ApplicationController
  def new
    @uesr = User.new
    2.times { @user.posts.build }
  end 
  
  def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = 'User created!'
      redirect_to root_url
    else
      render 'new'
    end
  end
  
  private
    
    def user_params
      params.require(:user).permit(:name, :age, posts_attributes: [:title, :body])
    end
end

細かいところは仕様によって変わるでしょうが, 最低限上記のような実装は必要になると思います. 詳しく見ていきましょう.

newアクション

newアクションでのポイントは以下のコードですね.

2.times { @user.posts.build }

単にUser.newで終わらせるのではなく, 空のPostオブジェクトもbuildしておく必要があります. 今回は例としてPostを2つ作成しましたが, ここは仕様に左右されるところでしょう.

Strong Parameters

Postモデルも保存するわけですから, Strong Parametersにおいてこれを許可しておかなくてはなりません. 書き方は以下の通りです.

posts_attributes: [:title, :body]

attributesは複数形です. また今回はUser has many Postsという関係ですから, postsも複数形となります.

createアクション

ここまでできていれば, Postオブジェクト込みのUserオブジェクトを作成することは簡単です.

User.new(user_params)

Userオブジェクト単体で作成するときと同じですね.

コントローラはどんな形でパラメータを受け取るのか

モデルとコントローラが実装できれば, integration testを書くことができます. そのためには, コントローラがどんな形でパラメータを受け取るのかを知る必要がありますね. その答えは以下の通りです.

params: { user: {
  name: 'Ruby on Rails',
  age: 14,
  posts_attributes: [
    { title: 'hey', body: 'Lorem ipsum' },
    { title: 'hi', body: 'Thank you!'}
  ]
} }

ビューの実装コード

フォームのコード例です.

<%= form_for @user do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  
  <%= f.label :age %>
  <%= f.number_field :age %>
  
  <%= f.fields_for :posts do |post_fields| %>
    <%= post_fields.label :title %>
    <%= post_fields.text_field :title %>
    
    <%= post_fields.label :body %>
    <%= post_fields.text_field :body %>
  <% end %>
  
  <%= f.submit "Submit" %>
<% end %>

簡単ですね! コントローラで2.timesとしているので, 自動的にPost用のフォームが2つ作られます.

Postモデルのバリデーション

はじめにPostモデルから以下のバリデーションを取り除きました.

validates :user_id, presence: true

これについて深掘りしていきます.

なぜこのバリデーションを取り除く必要があるのか

accepts_nested_attributes_forにより子モデルであるPostもまとめて保存できるようにした場合, 以下の流れで処理が進みます.

  • Userのバリデーション
  • Postのバリデーション
  • Userの保存処理
  • Postの保存処理

これは以下の記事を参考にしました. Rails5でnested attributesに詰まった話 - ウェブエンジニア珍道中

この記事にもあるように上のバリデーションを適用してしまうと,

Postのバリデーションの段階で, 未だ作られていないUserオブジェクトのidを要求してしまうため, 必ずバリデーション失敗する.

ということが起きてしまうわけです.

どのようにバリデーションを定義すべきか

失敗してしまうからといって, user_idの存在性を諦める必要はありません. ちゃんと別の方法が用意されています. 各モデルを以下のように編集しましょう.

class User < ApplicationRecord
  has_many :posts, inverse_of :user
  accepts_nested_attributes_for :posts
end

class Post < ApplicationRecord
  belongs_to :user, inverse_of :posts
  validates_presence_of :user
end

これで一括保存とバリデーションを両立することができます.