kinoppyd.dev

blog

products

accounts & contact

kinoppyd dev - page 4

kinoppyd blog

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では初期化のタイミングがすこし遅くなることが変更点となるでしょう。

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


BotはBotと会話するべきかどうか?

2018-12-13 02:22:54 +0900

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

BotとBotの関係性

突然ですが、このBotは無限ループを引き起こします。

require 'mobb'

on 'Yo' do
  'Yo'
end

Yoという発言を受け取り、Yoと自分も発言すると、自分の発言を拾って更にYoが無限にYoするからです。Mobbでは、これを防ぐために ignore_bot というコンディションフィルタを用意しています。

require 'mobb'

on 'Yo', ignore_bot: true do
  'Yo'
end

ignore_botコンディションを利用することによって、このonのブロックはBotに反応しなくなります。したがって、自分自身のYoの発言に自分で反応することもなくなります。(もっとも、これはReppハンドラが正しく人間とBotのアカウントを判断して情報を送ってくれるときに限るので、例えばBotと人間の区別がつかないサービスの場合はもちろん機能しません)

チュートリアルを紹介したエントリでも言及しましたが、この ignore_bot というフィルタは、今後デフォルトで機能するように変わる予定です。すなわち、次のバージョンのMobbでは、最初のコードと次のコードが全く等価になるということです。そのかわりに、次のバージョンからは react_to_bot と include_myself コンディションが用意されます。このコンディションをtrueに設定すると、BotはBotのメッセージに反応するようになります。

なぜこのような変更が入るかというと、BotとBotの関係性と安全性の問題があるからです。

Botフレームワークに限らず多くのライブラリは、記述の簡単さと、自由さと、安全性を天秤にかけることになります。Mobbは、その中でも簡単さと自由さに重点を置いていて、安全性に関しては優先度を下げていました(ここでいう安全性とは、ライブラリのセキュリティではなく、問題を起こしそうな記述を未然に防いでくれるかどうかということです)。そのため、Botが発したメッセージに関しても、ユーザーは特に何も考えることなくアクセスが可能で、Botに反応したくない場合はそれを明示的にユーザーが拒否すればいい、デフォルトでBotが可能な限り多くの情報に触れられるようにしたほうが良いという考え方で始まりました。しかしその一方で、この方針は無限ループを容易に引き起こしたり、無意味な発言を繰り返したりという行儀の良くない行動に関しても、記述するユーザーに責任を求めることになりました。

Mobbの安全性への姿勢は、リリース後に様々な人に触れてもらう中で、必要以上に軽視されているということが指摘されました。そのため、Mobbは次のバージョンから、いたずらに無限ループを起こしたり、他のBotにちょっかいを出す行儀の悪い行為をデフォルトで禁止し、そうしたい場合はユーザーに明示的に指示を求めるように変更することになりました。

次のバージョンからは、Mobbは自分を含む全てのBotの発言をデフォルトで無視します。これは、Botの安全性を高めるためです。そして、Botの発言を意図的に取り込みたい場合は、次のような記述が必要になります。

require 'mobb'

on 'Yo', react_to_bot: true do
  'Yo'
end

on 'Yo', react_to_bot: true, include_myself: true do
  'YoYo'
end

react_to_botコンディションは、自身を含めない他のすべてのBotの発言に対して反応するようになるコンディションです。そして、 include_myself コンディションは、自分自身の発言に対しても反応するようにするコンディションです。

ほぼすべてのケースにおいて、Botの作成者は react_to_bot コンディションを利用することでやりたいことがすべて実現すると思います。 include_myself は、昨日のエントリで紹介した chain/trigger 構文によって、自分自身への反応をする必要が無いからです。しかし、どうしてもすべてのメッセージに目を通したいというユーザー(例えば、自身を含む全ての発言を記録して、自身は何もしないBotを作りたいなど)のために、自分以外へのBotの反応、自分を含めたすべてのBotへの反応、という二段階の安全策を用意します。

記述性、自由度、安全性でバランスの取れた設計に

次のバージョンで追加されるコンディションで、Mobbは記述性をわずかに失い、安全性を大きく手に入れます。これは、必要なトレードオフです。しかし、Mobbの基本的な方針として、記述性と自由度を重視していく姿勢は変わりません。

次のバージョンのMobbは、年内にリリース予定です。よろしくおねがいします。


Mobbのメソッド呼び出しをチェーンする、 chain/trigger シンタックス

2018-12-12 02:02:01 +0900

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

Mobbにできないこと

これは、Mobb/Reppの未来の実装の話です。

Mobb(というよりも、Repp)の最大の特徴に、「発言を受け取り、返答を戻り値で返す」というシンプルな記述があります。これはとても強力でシンプルなルールですが、その一方でいくつか困ったことも起こります。

たとえば、Botにあるとても時間のかかる作業がさせたいとします。何も考えずに実装すると、その作業の起動トリガーを発言し、Mobbがそれを受け取り作業をし、完了したという文字列を返します。これは一見何もおかしなことは無いように見えますが、一つ大きな問題を抱えています。それは、時間のかかる作業の間、Botは無言で作業をこなしていて、その作業を起動させた人は、Botがちゃんと仕事をしているのか、それとも無言で死んだのかを区別する方法がありません。

次に、少し人間に優しい実装をしてみましょう。無言で死ぬのではなく、何かしらのエラーが発生した場合は、それを戻り値としてサービスに返せば、少なくとも何かエラーが発生したことはわかります。エラーメッセージの返し方によっては、何が原因で失敗したのかという細かい情報も得られるでしょう。これで概ね困ることは無いように思いますが、本当にそうでしょうか? この実装では、Botが作業を開始する指示を正しく受け取ってくれたのかどうかがわからないという問題が残っていませんか?

最後に、完全に人に優しい実装を考えてみましょう。作業の起動トリガーとなる発言を受けると、Botは即座に「作業を開始します」と返答します。その後、時間のかかる作業をやりながら、適宜「いまXXまで作業が終わりました」と教えてくれれば、ものすごく優しくないでしょうか? 少なくとも、コマンドを間違えてBotが起動してくれなかった場合などにはすぐ異常を検知できますし、何よりBotに愛着がわきます。

ところが、現状のMobb/Reppの構成では、このような適宜返事を返すBotと言うものを作ることが出来ません。なぜなら、最初に書いたとおり、Mobb/Reppはメソッドの戻り値がサービスの発言となり、途中経過で何かしらのアクションをすることが出来ないからです。そして、Reppはインターフェイスというその思想から、Mobb側にサービスに対してアクションをする方法は完全に秘匿されており、何もすることはできません。これは、よくあるBotフレームワークが、処理の途中で適宜サービスへのアクセスができることと大きな違いです。

しかし、Mobb/Reppは秒でクソボットを作れるフレームワークであると同時に、快適なBotを人々に提供することも軽視しているわけではありません。それでは、この問題を解決するにはどうすればいいでしょうか?

chain/trigger


Rubyを使って秒でBotを作るなら、秒でRedisだって使えなきゃ、やっぱり話にならないですよね?

2018-12-11 01:40:50 +0900

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

mobb-redis

昨日のエントリでは、MobbでActiveRecordを利用する方法を紹介して、Sinatraの資産をMobbが継承できるという話をしました。そして今日はRedisです。こっちは本当に秒でいけます。

https://github.com/kinoppyd/mobb-redis

まず、検証用のRedisを立てましょう。今ならDockerで秒です。

docker pull redis
docker run --rm -p 6379:6379 redis

次に、mobb-redisをインストールします。例ではBundlerを使います。

# frozen_string_literal: true
source "https://rubygems.org"

gem "mobb"
gem "mobb-redis"

最後にアプリケーションを書きます。

require 'mobb'
require 'mobb-redis'

register Mobb::Cache

on 'hello' do
  settings.cache.fetch('great') { "hello world #{Time.now}" }
end

はい、秒ですね。起動して動作を見てみましょう。

bundle exec ruby app.rb
== Mobb (v0.4.0) is in da house with Shell. Make some noise!
hello
hello world 2018-12-11 01:36:16 +0900
hello
hello world 2018-12-11 01:36:16 +0900

このように、返答の中の時間が変化していないので、Redisを経由していることがわかります。

redis-sinatra


Rubyを使って秒でBotを作るなら、秒でActiveRecord使えなきゃ話にならないですよね?

2018-12-10 02:35:19 +0900

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

ActiveRecord

ここ数日書いているエントリで、Mobbがいかに秒でBotを作れるすごいやつかというのは伝わったかと思います。しかし、世の中には秒で複雑で素敵なBotのアイディアが思い浮かぶ人もいます。そして、そういう人の多くは「いやいや、いくら秒でロジック書けても、DB使えないと話にならんでしょ」という人もいます。

そうです、Botのロジックは会話的なものが多いため、状態を記録したりデータを保持したりという行動を行いたいケースが非常に多いです。それでは、DBを使いましょう。RubyでDBといえば、ActiveRecordですね。ActiveRecord、使いたくないですか?

Mobbは、秒でBotを作るエンジニアを本気で応援するフレームワークです。当然、ActiveRecordも使えなくては話になりません。

安心してください、使えますActiveRecord。たった一つのgemを追加するだけで。

https://github.com/kinoppyd/mobb-activerecord

Usage


Mobb, test, spec

2018-12-08 02:21:24 +0900

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

最初にはっきり言っておくこと

このエントリは読まなくていい

Mobbのテスト

無い。現状、存在が無い。皆無。虚無。無。無って漢字に飽きてきた? じゃあ観測できないって言いかえる。

Mobbをテストするということ

Mobbをテストするということは簡単です。簡単ではないですが、書けます。なぜなら、MobbはSinatraのコピーとも言えるプロダクトなので、テストもコピーしてしまえば言いわけです。

ただ、現状そうもいっていない。私の時間ややる気の大小は感情的問題なので、定量的な判断からは除いておくと、Mobbの開発はあまり活発とは言えません。なので、単に書いていないだけです。これに関しては激しく切腹という言葉が脳裏に浮かびつつもどうにもならない問題で、時間とキーボードのストロークが解決してくれると思っています。どうしてMobbそのもののテストが無いのかという問題に立ち向かうとき、あなたはテストを書きましょう、私は怠けてしまったのです。としか言えません。

Mobbアプリケーションをテストするということ

Mobbアプリケーションをテストするということは、ほぼそのままの意味でReppアプリケーションをテストするということです。なぜなら、MobbはReppアプリケーションを作成するためのDLSであるからです。これをわかりやすく言うと、SinatraのテストをするということはRackアプリケーションをテストするということ、と同義です。

さて、Sinatraのテストはどう書くのか? 正確に言うと、Rackアプリケーションのテストはどう書くのか? それは、Rack::Testというものを使います。

https://github.com/rack-test/rack-test

これは、シンプルな意味でRackアプリケーションをテストするためのヘルパー群の集まりです。Rspecやtest-unitなどの各種テストスイーツを利用できます。

同じように、RackにもRack::Testというモジュールを追加する予定があります。現在鋭意製作中ですが、概ねRack::Testと似たような構成になると思っています。

Rack::Testは、内側でセッション情報などいろいろなものを頑張って扱っていますが、幸いMobb/Repp構成においてセッション情報とかいうものはあまり関係無かったりするので、Rack::Testよりは多少シンプルになるのではないでしょうか。

最後に

ネタが切れてきた


Reppのインターフェイス

2018-12-07 03:24:06 +0900

この記事は Mobb/Repp Advent Calendar の7日目です

Reppのインターフェイス

少し前のエントリで、Reppはインターフェイスだと解説しましたが、実はそのインターフェイスの定義をきちんと書いたドキュメントは存在しません。それは何故かと言うと、まだインターフェイスをどのように固めれば柔軟に今後対応していけるかという確信が、書いている本人である私にないからです。現状の仮実装として、Slackを事実上の対象サービスとして想定した記述ができるように、次のように実装されています。

  • environmentのハッシュを引数に一つ受け取るcallメソッドが実装されている

  • 2つの要素からなる配列を返す。サービスに投稿する文字列が1つ目の要素、各サービスごとにオプショナルに解釈してほしい内容をハッシュで2つめの要素に入れる

ReppはRackの模倣をして作られた、各種サービスとBotフレームワークとの間をつなぐ取り決めですが、Rackの取り扱うHTTP(正確にはWebサーバー)の世界とは違い、Botが接続するSlackやShellやChatWorkやLINEや、あるいはHTTPかもしれないサービスとの接続はその形式が多種多様に渡り、抽象化するとどのようにまとまるのかをうまく調整することが難しいのです。

たとえば、SlackにはAttachmentsというチャットの発言とは別にチャットの内容を装飾するための取り決めがありますが、それは他のサービスにはありません。しかし、Reppとしては存在したりしなかったりする内容に関してもきちんと橋渡しをする必要があります。