kinoppyd.dev

blog

products

accounts & contact

kinoppyd dev - page 4

kinoppyd blog

MobbアプリケーションをRack上で起動できるか?

2018-12-22 23:40:51 +0900

このエントリは Mobb/Repp Advent Calendar の二十二日目です

Mobbアプリケーション is Rackアプリケーション?

結論から言うと、動きません。

試しにこんなアプリを書いて起動してみました。

app.rb

require 'mobb/base'

class MyApp < Mobb::Base
  before do
    p @env
  end
end

config.ru

require './app'

run MyApp.new

起動

bundle exec rackup

MobbはRackのアプリケーションとほぼ互換なので、理屈の上では動きそうなものですが、動きませんでした。理由としては、Mobbがサービスからの情報を受け取ったときに処理するfilterやhandle_eventメソッドの中で使われている、process_eventメソッドにありました。

process_event の中身は次のようなメソッドです。

def process_event(pattern, conditions, block = nil, values = [])
  res = pattern.match?(@env.body)
  catch(:pass) do
    conditions.each { |c| throw :pass unless c.bind(self).call }

    case res
    when ::Mobb::Matcher::Matched
      block ? block[self, *(res.matched)] : yield(self, *(res.matched))
    when TrueClass
      block ? block[self] : yield(self)
    else
      nil
    end
  end
end

この中で、 @env.body を参照している箇所に問題がありました。Reppと違い、Rackの送ってくるenvオブジェクトには、bodyというメソッドが存在しないからです。

比較のために、Mobbが参考にしているSinatraのprocess_routeメソッドを見てみましょう。

def process_route(pattern, conditions, block = nil, values = [])
  route = @request.path_info
  route = '/' if route.empty? and not settings.empty_path_info?
  route = route[0..-2] if !settings.strict_paths? && route != '/' && route.end_with?('/')
  return unless params = pattern.params(route)

  params.delete("ignore") # TODO: better params handling, maybe turn it into "smart" object or detect changes
  force_encoding(params)
  original, @params = @params, @params.merge(params) if params.any?

  regexp_exists = pattern.is_a?(Mustermann::Regular) || (pattern.respond_to?(:patterns) && pattern.patterns.any? {|subpattern| subpattern.is_a?(Mustermann::Regular)} )
  if regexp_exists
    captures           = pattern.match(route).captures.map { |c| URI_INSTANCE.unescape(c) if c }
    values            += captures
    @params[:captures] = force_encoding(captures) unless captures.nil? || captures.empty?
  else
    values += params.values.flatten
  end

  catch(:pass) do
    conditions.each { |c| throw :pass if c.bind(self).call == false }
    block ? block[self, values] : yield(self, values)
  end
rescue
  @env['sinatra.error.params'] = @params
  raise
ensure
  @params = original if original
end

最初に参照しているのが、 @request.path_info というメソッドで、これはどう考えてもHTTPに存在し、チャットボットに存在しない概念です。

残念ながら、MobbをRackで動かすという試みは、このRackとReppの微妙な世界観の違いで頓挫しました。

MobbをRackで動かせるべきか?

答えはNoです。MobbはSinatraを最大限にリスペクトしていますが、Sinatraの世界観とは違うものです。もちろん動かせれば面白いとは思いますが、MobbをRackに対応させる理由は全くありません。


Mobbを使った複数サービス間のゲートウェイを実現する方法

2018-12-21 23:32:58 +0900

このエントリは、 Mobb/Repp Advent Calendar の二十一日目です

サービス間ゲートウェイ

Slackの発言をIRCに転送したり、HTTPアクセスを受け取ってSlackに投稿するIncomming Webhook のようなものをMobbで書きたい場合にはどうすれば良いでしょうか?

Mobbのロジックに書く

一つ目の答えは、Mobbアプリケーションのロジックに、転送先のサービスのクライアントを記述して、入力をすべてそちらに飛ばし、ブロックの戻り値はnilにして入力元のサービスには何も返さない方法です。多分これは一番直感的で楽だと思います。

Reppハンドラを書く

もう一つの手段は、専用のReppハンドラを用意してしまうことです。デフォルトのハンドラでは、入力と出力のソースが同じため、容易にゲートウェイの動作をさせることはできません。しかし、入力と出力を別々にもつReppハンドラを記述することはできます。


IoTしてますか? 難しいですよね? でもお手軽にWxBeacon2を使って室内環境監視ダッシュボードとか作れますよ?

2018-12-21 01:00:22 +0900

このエントリは、 dwango Advent Calendar の二十一日目です

TL; DR

  • WxBeacon2を使って、簡易室内環境モニタを作ります

  • どこでも確認したいので、DBとフロントはWebに置きます

  • WxBeacon2 + Python + Fluentd + InfluxDB + Grafana + DockerCompose

お手軽に室内環境を監視したい

世の中IoTとかMakerとかいう言葉が流行り始めて数年が立ちましたが、Raspberry Piは買ったものの特に何を作るわけでもなく完全に腐らせている方は、私の他にも多いのではないでしょうか。電子工作の本とか買ってみて、いろいろなんかやろうとか思ってみはしたものの、本業や趣味のコーディングのほうが楽しくて、あまり真剣に向き合って来ませんでした。

ブレッドボードになんか刺したり、秋月や千石に行ってパーツを探したり、はんだ付けしたり、なんかかっこいいものを作って人にオォーって言われたかったりしたかった人生なんですが、まあそれはそれとして漠然と何かを作れる人に憧れがありました。そんなとき、技術書展5に出た際にブースを手伝ってくれた友人が、何やらカバンに奇妙な物体をつけているのを見ました。話を聞くと、どうやらそいつは気象系のセンサをいろいろ詰め込んだ便利なやつで、スマホアプリと接続して情報を見たり、BLEでPCとつなげたりもできるとのことでした。実際にスマホアプリを見てみると、気温や湿度、気圧に周囲の光量や騒音まで定期的に取得していました。それはなんだと聞くと、WxBeacon2だと言われました。

WxBeacon2

WxBeacon2とは、Weather News が販売している簡易気象観測器です。Weather News のアプリからポイントを貯めると貰えるらしいですが、まどろっこしいのでお金を払って買うことも出来ます。本体と消費税と送料込みで、5000円しないので、パパっと買ったほうが良いです。

WxBeacon2は、内容としてはオムロン製の2JCIE-BL01というIoTセンサのOEMです。2JCIEシリーズには、WxBeacon2と同型のバッグ型センサの他に、USBドングルの形をしたセンサや、PCB型のセンサも販売されています。ただ不思議なことに、日本や海外のどのセンサ通販サイトを見ても、USB型やBAG型は軒並み単価10000円を超え(しかもボリュームディスカウントも薄い)、PCB型に至っては売っているサイトすら見つけられません。そんな中、なぜかWeather News は半額以下の5000円未満でBAG型のOEM品を販売していて、ダントツで安く手に入れられるのです。なので、どうしてもUSB型が必要とかいう場合を除いて、Weather News で買うのが最も安く手に入れられる方法です。

そしてWxBeacon2もとい2JCIE-BL01は、なぜかGitHubに通信用のサンプルコードが置かれています。多分公式ではないと思うのですが(他にOmronのリポジトリも無いし、そもそもOrganizationもないので)、ここに載せられているサンプルプログラムだけで十分なので、こちらを参照します。

https://github.com/OmronMicroDevices/envsensor-observer-py

今回作りたいもの


Mobbのマッチングにどれもヒットしなかった場合のフック

2018-12-20 17:02:18 +0900

このエントリは、 Mobb/Repp Advent Calendar の二十日目です

どれにもマッチしなかった

これは次のバージョンで実装される機能の話です。

Mobbはonキーワードでマッチングを登録し、サービスからの入力に対して一致チェックを行い、一致すればブロックを実行します。しかし、どこにもマッチしなかった入力は虚空に消えていきます。

Webフレームワークの場合、どこにもマッチしなかった場合には404が帰ります。そのため、404のページにはなにを表示するかといった設定ができます。しかし、チャットBotはWebのようにリクエストとレスポンスが対象的な世界ではないので、特にそういうものはありません。

しかしその一方で、どこにもマッチしなかった入力に対してなにか処理をしたいという需要はあると思います。それは、マッチングの対象ではない要素に対してなにかをしたい場合であったり(フィルタを使うという手もありますが)、すべての入力に対して一律なにか処理をしたりという場合です。

require 'mobb'

on 'Hi' do
  'Yo'
end

on_unregistered do
  # デバッグメッセージ
  puts "#{@env.body} は登録されているパターンにマッチしませんでした"
end

on_unregistered キーワードは、すべてのマッチング処理の最後にチェックされ、どこにも一致しているものがない場合は必ずこのブロックを実行するというものです。うえのbotでは、マッチングのデバッグを行うために、一致しなかったすべての処理を出力しています。


Mobbにおけるマッチのパッシング

2018-12-19 15:28:43 +0900

このエントリは Mobb/Repp Advent Calendar の十九日目です

マッチのパッシング

この機能は次のバージョンにおいて実装される予定です。

次のようなBotを作成し、「hello Mobb」というメッセージを送った場合、得られる結果は「Yo」です。

require 'mobb'

on /hello (\w+)/ do |name|
  'Yo'
end

on 'hello Mobb' do
  'Survival of the fittest'
end

これは、Mobbのパターンマッチは定義した順番にチェックされるので、最初の /hello (\w+)/ がすべての hello で始まるメッセージを吸収してしまい、次に定義されている ‘hello Mobb’ にマッチすることは決してありません。

この例は非常に極端な例ですが、特定のケースにおいてマッチングをパスしたいことは発生すると思われます。そのため、次のバージョンではpassキーワードが導入されます。

require 'mobb'

on /hello (\w+)/ do |name|
  pass if name.start_with?('M')
  'Yo'
end

on 'hello Mobb' do
  'Survival of the fittest'
end

passキーワードは、呼び出されるとその場でブロックの評価を停止し、on/receive キーワードのマッチングを再開します。上のBotでは、nameがMで始まる場合は、 on /hello (\w+)/ のブロックを抜け、次の on ‘hello Mobb’ にマッチします。その結果、得られる返答は「Yo」ではなく「Survival of the fittest」になります。(もちろんこの例では、Mobb以外のMで始まる名前を送るとすべてのケースでなにも返答しなくなってしまいますが)

Next Mobb

年内リリースがんばります


Mobb+GitQueueでバージョン管理付きのTODO Botを作る

2018-12-18 15:50:08 +0900

このエントリは Mobb/Repp Advent Calendar の十八日目です

Mobb + GitQueue = バージョン管理つきTODO bot

ちょうど一年くらい前、Gitをバックエンドとしたスタックを実装した話をしました。

http://tolarian-academy.net/task-manage-bot-with-git/

これを実際にMobbと組み合わせたBotを作成してみます。

require 'mobb'
require 'git_queue'

STORAGE = './storage'

set :service, 'slack'
set :name, 'YOUR BOT NAME'

helpers do
  def stack
    @stack ||= begin
                 GitQueue::Storage.create(STORAGE) unless File.exists?(STORAGE)
                 GitQueue::Queue.new(STORAGE)
               end
  end
end

set(:channel_filter) { |name| condition { @env.channel == name } }

on /add (.+)/, ignore_bot: true, reply_to_me: true, channel_filter: 'CXXXXXXX' do |task|
  stack.push(task)
  "#{task} を追加した"
end

on /now/, ignore_bot: true, reply_to_me: true, channel_filter: 'CXXXXXXX' do
  task = stack.queue.first
  "#{task} をやれ"
end

on /done/, ignore_bot: true, reply_to_me: true, channel_filter: 'CXXXXXXX' do
  task = stack.pop
  "#{task} が終わった"
end

# TODO: Fix Mobb capture bug
on /switch \d+\s+\d+/, ignore_bot: true, reply_to_me: true, channel_filter: 'CXXXXXXX' do
  target = /(\d+)\s+(\d+)/.match(@env.body).captures
  t1 = target[0].to_i
  t2 = target[1].to_i
  queue = stack.queue
  return "そんなにタスクは無い" if queue.size <= t1 || queue.size <= t2
  task1 = queue[t1]
  task2 = queue[t2]
  stack.switch(t1, t2)
  "#{task1}#{task2} を入れ替えた"
end

on /ls/, ignore_bot: true, reply_to_me: true, channel_filter: 'CXXXXXXX' do
  "タスク一覧\n" + stack.queue.map { |t| "- #{t}" }.join("\n")
end

GitQueueというGemを使ってスタックを作り、それをMobb経由で操作しています。

スクリーンショット 2018-12-18 15.45.18

まあまあ便利です。


Mobb製のBotになにか処理をさせたが、何も反応を返したくないときはどうするのか

2018-12-17 01:18:13 +0900

このエントリは Mobb/Repp Advent Calendar の十七日目です

Botに何も発言をさせたくないとき

Mobbで作られたBotは、ブロックの戻り値の文字列をサービスに投稿します。しかし、ブロックを実行したあとに何も発言したくないときはどうすればいいのでしょうか? 答えは、nilを返せばいいのです。

require 'mobb'

on /.+/ do 
  File.write("#{Time.now}.txt, @env.body)
  nil
end

このBotは、すべての発言に反応し、その発言をファイルに書き込み、何も投稿せずにそのまま処理を終了します。しかし、このnilを返す方法はやや分かりづらいとの指摘を受けたので、次のバージョンでは say_nothing キーワードと silent コンディションを用意します。

require 'mobb'

on /hello \w+/ do |name|
  say_nothing if name != settings.name
  "hello"
end

on /.+/, silent: true do
  File.write("#{Time.now}.txt", @env.body)
end

say_nothing キーワードは、 say_nothing が呼び出された場合に、そのブロックがなんの値を返そうがサービスにポストを行いません。つまり、nilを返したときと同じ挙動をします。

silentはコンディションなので、現時点でも任意で追加可能ですが、Mobbのデフォルトに追加します。内容としては、ブロック実行後の戻り値を見て、何が入っていようがnilで上書きするコンディションを追加します。おそらく、実装としては次のようなコードになるのではないでしょうか?

require 'mobb'

helpers do
  def silent(cond) do
    dest_condition do |res|
      res[0] = nil
    end
  end
end

on /.+/, silent: true do
  "this string is not post to service"
end

次のバージョンのMobbにご期待下さい

以上です


Mobbのcronを秒単位で動かす

2018-12-16 02:24:17 +0900

このエントリは Mobb/Repp Advent Calendar の十六日目です

MobbのCronは毎秒実行されない

MobbのCronは、CronのSyntaxをパースするため、最小の実行単位が分までしか設定できません。しかし、世の中には意外と毎秒何かを監視するという行動に需要があり、Mobbで作られたBotも毎秒何かを実行させたいという人は多いので、CronSyntaxを使わず毎秒実行するトリガーを、次のバージョンで追加することにしました。

require 'mobb'

every_seconds do
  # act every seconds
end

every/cron キーワードはすでに使用されているため、新しいキーワードを設定する必要があります。every_secondsキーワードです。every_secondsに渡されたブロックは、毎秒ブロックの中身を実行します。注意しなくてはいけないのは、every_secondsは1つのBotで1度しか設定できない(複数設定された場合は最初に設定したものが優先される)ということです。

なぜevery_secondsは一つしか設定できないかというと、この設定は任意のn秒で実行されるわけではなく、毎秒実行されることを強制するからです。実際にBotを作成するときに求められる需要は、特定の秒になにかしたいではなく、毎秒なにかをしたい、というケースが想定されるからです。特定の秒に何かをしたい場合は、毎秒実行されている処理の中でその秒を判断すれば、実行できます。

every_secondsの実行でネックになるのは、0秒目、つまりcronが実行される可能性がある毎分の0秒だけ、処理がスキップされるという点です。これは、Mobbが送っている毎秒のトリガーが、先にcronのブロックにマッチするためです。0秒にevery_seconds と every/cron が競合するのは良くないことなので、どちらも実行されるようにはしたいですが、ひとまずミニマルな実装を行うため、0秒には every_seconds が走りません。

every_secondsは次のバージョンで実装されます

お楽しみに


MobbのLogger

2018-12-15 03:04:57 +0900

このエントリは、 Mobb/Repp Advent Calendar の十五日目です

MobbのLogger

ネタ切れを起こしたので、時間を稼ぎます。

MobbのLoggerは、当然SinatraのLoggerを参考にして実装されました。そしてSinatraのLoggerは、RackのLoggerがそのままrequest経由で渡されてくるものでした。

MobbはSinatraをベースにしているので、当然Loggerに関してもRackをベースにしたReppから渡ってくるものです。

残念ながらというかなんというか、Loggerの存在を完全に忘れて今まで実装を進めてしまったので、Loggerに関する実装は次のバージョンでどうにかしようと思っています。その時は、MobbだけではなくReppにもそれなりの変更が入るでしょう。

require 'mobb'

on /hello/ do
  logger.info('call hello')
  'hi'
end

Sinatraと同じ、こういう使い方ができることを想定しています。


Mobbの正規表現解釈と、MatchDataの行方

2018-12-14 02:32:30 +0900

このエントリは、 Mobb/Repp Advent Calendar の十四日目です

Mobbの扱う正規表現

Mobbでは、 on/receive メソッドの引数として正規表現を渡すことが出来ます。

require 'mobb'

on /add user (\w+)/ do |name|
  # name には、正規表現のキャプチャ結果が入る
end

この部分は現在、次のようなコードで解釈がされています。

def process_event(pattern, conditions, block = nil, values = [])
  res = pattern.match?(@env.body)
  catch(:pass) do
    conditions.each { |c| throw :pass unless c.bind(self).call }

    case res
    when ::Mobb::Matcher::Matched
      block ? block[self, *(res.matched)] : yield(self, *(res.matched))
    when TrueClass
      block ? block[self] : yield(self)
    else
      nil
    end
  end
end

Reppからの入力が正規表現に一致した場合、 Mobb::Matcher::Matched オブジェクトが作成され、その中のキャプチャ結果を on/receive のブロックに対して引数として渡しています。

このように、Mobbは正規表現のマッチ結果を受け取れる能力はあるのですが、あるひユーザーのひとりに「名前付きキャプチャを使いたいから、RegexpのMatchDataをそのまま触らせって欲しい」という要望を伝えられました。

そのような用途もあることは理解できるので、どうにかMatchDataをユーザーが触れるように提供してみようと思い、次のような構文を考えています。

require 'mobb'

on /(?<word>\w+) \k<word> \k<word>/ do
  matched[:word]
end

このBotに対して、 “hey hey hey” と呼びかけると、 “hey” という文字列が返ってくるようにしたいとおもっています。つまり、 on/receive の引数に正規表現をとった場合、matchedというアクセサがブロックの中で利用でき、呼び出すとRegexp#matchの戻り値が得られるような構文を考えています。

これらは、Sinatraでいうところの request/response/params といったアクセサと同じ扱いになりますが、SinatraがRackからの呼び出しの直後にこれらの変数を初期化するのに対し、Mobbでは初期化のタイミングがすこし遅くなることが変更点となるでしょう。

機能追加のリクエストお待ちしてます