Translate

2017年4月10日月曜日

Hubot Document: Scripting を翻訳して、Hubotチャットボットの書き方を学習する

 Hubotフレームワークは、coffee scriptかJava Scriptで書かなければならない。
それにいわゆるプラグインのようにスクリプトを配置していく構造なので
Hubotが決めた書式に従ってコードを書かなくてはならない。

それにいろいろ予約語というか、
メタデータなどの扱いなんかもどうやればいいのか
しっておきたいし...

ということで本家サイトの

Hubot Documentation: Scripting
https://hubot.github.com/docs/scripting/

を翻訳することにした。
以下翻訳文ですが参照の場合は at your own riskでお願いします。
----------

Hubot Docmentation: Scripting


箱から取り出したばかりの Hubotは、それほど多くではありませんが拡張可能でスクリプト化可能なロボットのフレンズです。 コミュニティによって書かれ管理されている数百のスクリプト があり、簡単に自分のスクリプトを書くこともできます。 hubotの @scripts@ ディレクトリにカスタムスクリプトを作ったり、 コミュニティと共有するためのスクリプトパッケージを作る こともできますよ!


スクリプトの解剖学


hubot を作成すると、ジェネレータは scripts ディレクトリもあわせて作成します。 このディレクトリをのぞけは、スクリプトのサンプルを確認することができます。 スクリプトをスクリプトにするには、次のことが必要です:

  • hubot ロードパス(デフォルトは src/scripts および scripts )を通したディレクトリに配置
  • 拡張子 .coffee もしくは .js にする
  • 関数をエクスポートする

関数をエクスポートするとは、次のようにスクリプトを記述するという意味です:

module.exports = (robot) ->
  # コードをここに記述

パラメータ robot は、あなたのロボットのインスタンスをあらわしています。 これで、いくつかの素晴らしいスクリプトを開始することができきるようになりました。


hear respond


HuBot はチャットボットをつくるためのフレームワークなので、最も一般的なやりとりはメッセージに基づいています。 Hubot は、ルーム内で発言されたメッセージを聞いたり( hear )、直接そのメッセージに返信する( respond )ことができます。 どちらのメソッドも、正規表現とコールバック関数をパラメータとして取ります。 例えば:

module.exports = (robot) ->
  robot.hear /badger/i, (res) ->
    # コードをここに記述

  robot.respond /open the pod bay doors/i, (res) ->
    # コードをここに記述
robot.hear /badger/ コールバック関数は、メッセージのテキストが正規表現とマッチするたびに呼び出されます。 例えば、次のようなメッセージの場合呼び出されます:

  • Stop badgering the witness
  • badger me
  • what exactly is a badger anyways
robot.respond /open the pod bay doors/i コールバック関数は、ロボットの名前またはエイリアスの直前にあるメッセージのためだけに呼び出されます。 ロボットの名前が HAL でエイリアスが / の場合、このコールバック関数は次のメッセージでトリガされます:

  • hal open the pod bay doors
  • HAL: open the pod bay doors
  • @HAL open the pod bay doors
  • /open the pod bay doors

次のメッセージの場合は、呼び出されません:

  • HAL: please open the pod bay doors :respond はロボット名直後のテキストにバインドされているためです。
  • has anyone ever mentioned how lovely you are when you open the pod bay doors? :ロボット名がないためです。

send および reply


パラメータ res Response インスタンスです(歴史的にこのパラメータは msg でした。このように他のスクリプトで使用されることがあります)。 res を使用すると、書き込まれたチャットルームにメッセージを送信したり( send )、、任意のチャットルームにメッセージを送信したり( emote )(ただし、指定されたアダプタがサポートしている場合のみ)、メッセージを送信した人に返信( reply )することができます。 例えば:

module.exports = (robot) ->
  robot.hear /badger/i, (res) ->
    res.send "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS"

  robot.respond /open the pod bay doors/i, (res) ->
    res.reply "I'm afraid I can't let you do that."

  robot.hear /I like pie/i, (res) ->
    res.emote "makes a freshly baked pie"
robot.hear /badgers/ コールバック関数は、だれが発言したかに関係なく、指定された通りに正確にメッセージ"Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS"を送信します。

もしユーザ Dave"HAL: open the pod bay doors"と発言したのであれば、 robot.respond /open bay doors/i コールバック関数からメッセージ"Dave: I'm afraid I can't let you do that."が送られます。


データのキャプチャリング


ここまでは静的な応答を扱っていましたが、もっと面白いもの、機能的には退屈なものもあります。 res.match には、着信メッセージを正規表現と照合( match )した結果が格納されています。 これは単に JavaScript関数 match() と同じで、最終的には式に一致する完全なテキストであるインデックス0を持つ配列になります。 取得グループを含めると、 res.match が作成されます。 たとえば、次のようなスクリプトを更新するとします:

  robot.respond /open the (.*) doors/i, (res) ->
    # コードをここに記述


Daveが"HAL: open the pod bay doors"と発言したならば、 res.match[0] は"open the pod bay doors"で、 res.match[1] は単に "pod bay" となります。これを使ってよりダイナミックなチャットボットを始めることができます:

  robot.respond /open the (.*) doors/i, (res) ->
    doorType = res.match[1]
    if doorType is "pod bay"
      res.reply "I'm afraid I can't let you do that."
    else
      res.reply "Opening #{doorType} doors"

HTTP通信の構築


Hubot は、サードパーティのAPIを統合して使用するために、あなたの代わりにHTTPコールを行うことができます。 これは、 robot.http で利用可能な node-scoped-http-client インスタンスを介して行うことができます。 最も単純なケースは次のようになります:


  robot.http("https://midnight-train")
    .get() (err, res, body) ->
      # コードをここに記述

POSTは次のように記述します:

  data = JSON.stringify({
    foo: 'bar'
  })
  robot.http("https://midnight-train")
    .header('Content-Type', 'application/json')
    .post(data) (err, res, body) ->
      # コードをここに記述


err は、処理中に発生したエラーをあらわします(発生した場合)。一般的にこれをチェックし、それに応じて処理したいと思うでしょう:

  robot.http("https://midnight-train")
    .get() (err, res, body) ->
      if err
        res.send "Encountered an error :( #{err}"
        return
      # (成功した場合の)コードをここに記述


res node http.ServerResponse インスタンスです。 node-scoped-http-client を使用する場合、ほとんどのメソッドは重要ではありませんが、興味があるのは statusCode getHeader です。 statusCode を使用してHTTPステータスコードをチェックします。通常 200 以外の場合は何か問題が発生したことを意味します。 レートリミットを確認するなど、ヘッダを表示するには getHeader を使用します。

  robot.http("https://midnight-train")
    .get() (err, res, body) ->
      # ここにエラーチェックコードを記述

      if res.statusCode isnt 200
        res.send "Request didn't come back HTTP 200 :("
        return

      rateLimitRemaining = parseInt res.getHeader('X-RateLimit-Limit') if res.getHeader('X-RateLimit-Limit')
      if rateLimitRemaining and rateLimitRemaining < 1
        res.send "Rate Limit hit, stop believing for awhile"

      # 残りのコードを記述

body はレスポンスボディを文字列として扱います。おそらく最も気になるものです:

  robot.http("https://midnight-train")
    .get() (err, res, body) ->
      # ここにエラーチェックコードを記述

      res.send "Got back #{body}"



JSON


APIと対話する際の最も簡単な方法は、余分な依存関係を必要としない JSON です。  robot.http を呼び出すときには、通常、 Accept ヘッダを設定して、APIにあなたが期待しているものを提供する必要があります。  body を取得したら、 JSON.parse で解析することができます:

  robot.http("https://midnight-train")
    .header('Accept', 'application/json')
    .get() (err, res, body) ->
      # ここにエラーチェックコードを記述

      data = JSON.parse body
      res.send "#{data.passenger} taking midnight train going #{data.destination}"

APIにエラーが発生し、JSON の代わりに通常のHTMLエラーをレンダリングしようとする場合など、非 JSON を戻すことは可能です。 安全な側になるためには、 Content-Type をチェックし、解析中にエラーをキャッチする必要があります。

  robot.http("https://midnight-train")
    .header('Accept', 'application/json')
    .get() (err, res, body) ->
      # ここに err および response ステータス確認コードを記述

      if response.getHeader('Content-Type') isnt 'application/json'
        res.send "Didn't get back JSON :("
        return

      data = null
      try
        data = JSON.parse body
      catch error
       res.send "Ran into an error parsing JSON :("
       return

      # コードを記述




XML


バンドルされたXML解析ライブラリがないため、XML APIは実装が難しくなります。 詳細については、このドキュメントの範囲を超えていますが、ここにいくつかのライブラリがあります:


スクリーンスクレイピング


APIがない場合は "スクリーンスクレイピング" が使えるの可能性が常にあります。 詳細についてはこのドキュメントの範囲を超えていますが、ここではいくつかのライブラリをチェックアウトしています:
  • cheerio (jQueryのおなじみの構文とAPI)
  • jsdom (W3C DOMのJavaScript実装)

高度な HTTP/HTTPS 設定


前述のように、hubot は node-scoped-http-client を使用して、HTTPおよびHTTPS要求を行うための単純なインタフェースを提供します。 その傘下では、nodeの組み込み http https ライブラリを使用していますが、最も一般的な種類の対話に簡単なDSLを提供しています。

http https のオプションをより直接的に制御する必要がある場合は、 http https に渡される node-scoped-http-client 上の第2引数を robot.http に渡します:

  options =
    # CAに対してサーバ証明書を検証しないでください、怖い!
    rejectUnauthorized: false
  robot.http("https://midnight-train", options)

さらに、 node-scoped-http-client があなたに合っていない場合は、 http https を直接使うことも、 request のような他のノードライブラリを使うこともできます。


ランダム


一般的なパターンは、コマンドをhearしたり、コマンドにrespondしたり、ランダムな面白い画像やテキスト行を可能な限り配列から送信することです。 JavaScriptやCoffeeScriptですぐにこれを行うのは面倒ですので、Hubot には便利なメソッドがあります:

lulz = ['lol', 'rofl', 'lmao']

res.send res.random lulz

トピック


アダプタがそれをサポートしている場合は、Hubot は部屋のトピック変更に反応することができます。
module.exports = (robot) ->
  robot.topic (res) ->
    res.send "#{res.message.text}? That's a Paddlin'"



入退室


アダプタがそれをサポートしている場合は、Hubot はユーザの入退室を確認することができます。

enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']
leaveReplies = ['Are you still there?', 'Target lost', 'Searching']

module.exports = (robot) ->
  robot.enter (res) ->
    res.send res.random enterReplies
  robot.leave (res) ->
    res.send res.random leaveReplies


カスタムリスナ


上述のヘルパは、平均的なユーザが必要とする(聞く(hear)、応答する(respond)、入室する(enter)、退室する(leave)、トピック(topic))機能のほとんどをカバーしていますが、時にはリスナに対して非常に特殊なマッチングロジックを使用したいことがあるでしょう。 その場合、 listen を使用して正規表現の代わりにカスタムマッチ関数を指定することができます。

リスナがコールバックを実行する際に、match 関数は真理値を返却します。 次に match関数の返す真偽値が response.match としてコールバック関数に渡されます。

module.exports = (robot) ->
  robot.listen(
    (message) -> # マッチ関数
      # Steve の発言に時々返信する
      message.user.name is "Steve" and Math.random() > 0.8
    (response) -> # 標準リスナコールバック
      # 彼が存在することがどれほど幸せかをスティーブに知らせてください
      response.reply "HI STEVE! YOU'RE MY BEST FRIEND! (but only like #{response.match * 100}% of the time)"
  )

複雑なマッチ処理例については、デザインパターンのドキュメントを参照してください。


環境変数


Hubotは、 process.envを使用して、他のNodeプログラムと同じように、実行している環境にアクセスできます。 これは、スクリプトの実行方法を設定するのに使用できます。規約は接頭辞 HUBOT_を使用します。

answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING

module.exports = (robot) ->
  robot.respond /what is the answer to the ultimate question of life/, (res) ->
    res.send "#{answer}, but what is the question?"


スクリプトが定義されていない場合、スクリプトをロードできるか確認し、Hubot 開発者に定義方法を伝えたり、何かをデフォルトにするよう注意してください。 スクリプト作成者は、致命的なエラー(例えば、hubotが終了するかどうか)を判断して、それに依存するスクリプトを構成する必要があると説明します。可能であれば、それが理にかなっているときは、他の設定をせずにスクリプトを実行することを推奨します。

ここではデフォルトにすることができます:

answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING or 42

module.exports = (robot) ->
  robot.respond /what is the answer to the ultimate question of life/, (res) ->
    res.send "#{answer}, but what is the question?"

定義されていない場合は、ここで終了します:

 answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
unless answer?
  console.log "Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again"
  process.exit(1)

module.exports = (robot) ->
  robot.respond /what is the answer to the ultimate question of life/, (res) ->
    res.send "#{answer}, but what is the question?"

最後に、robot.respondを更新してチェックします:

answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING

module.exports = (robot) ->
  robot.respond /what is the answer to the ultimate question of life/, (res) ->
    unless answer?
      res.send "Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again"
      return
    res.send "#{answer}, but what is the question?"

依存関係


Hubotは npm を使用してその依存関係を管理します。 パッケージを追加するには、パッケージを package.jsondependencies  に追加します。 たとえば、 lolimadeupthispackage 1.2.3 を追加したい場合は、次のようになります:

  "dependencies": {
    "hubot":         "2.5.5",
    "lolimadeupthispackage": "1.2.3"
  },

hubot-scripts からスクリプトを使う場合は、追加するスクリプトの Dependencies 文書を書き留めておいてください。 それらは package.json にコピー&ペーストできる形式でリストされていますが、有効なJSONにするために必要に応じてカンマを必ず追加してください。

タイムアウトとインターバル


Hubot は JavaScript のビルトイン setTimeout を使用してコードを遅延実行できます。 これはコールバックメソッドとそれを呼び出すまでに待つ時間をとります:

module.exports = (robot) ->
  robot.respond /you are a little slow/, (res) ->
    setTimeout () ->
      res.send "Who you calling 'slow'?"
    , 60 * 1000

さらに、Hubot は setInterval を使用してインターバルでコードを実行できます。 コールバックメソッドと、コール間の待機時間が必要です:

module.exports = (robot) ->
  annoyIntervalId = null

  robot.respond /annoy me/, (res) ->
    if annoyIntervalId
      res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH"
      return

    res.send "Hey, want to hear the most annoying sound in the world?"
    annoyIntervalId = setInterval () ->
      res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH"
    , 1000

  robot.respond /unannoy me/, (res) ->
    if annoyIntervalId
      res.send "GUYS, GUYS, GUYS!"
      clearInterval(annoyIntervalId) ->
      annoyIntervalId = null
    else
      res.send "Not annoying you right now, am I?"

HTTPリスナ


Hubot には、HTTP要求を処理するための Express Webフレームワークのサポートが含まれています。 環境変数 EXPRESS_PORT または PORT で指定されたポート(この順序で優先されます、デフォルトは8080)をlistenします。 Express アプリケーションのインスタンスは robot.router から入手できます。 EXPRESS_USEREXPRESS_PASSWORD を指定して、ユーザー名とパスワードで保護することができます。 EXPRESS_STATIC を設定することにより、自動的に静的ファイルを提供することができます。

この最も一般的な使い方は、プッシュ時のwebhooksによるサービスにHTTPエンドポイントを提供して、チャットに情報を表示することです。
module.exports = (robot) ->
  # 期待値:ルームがアダプタごとに異なる場合、数値ID、名前、トークン、またはその他の値である可能性があります。
  robot.router.post '/hubot/chatsecrets/:room', (req, res) ->
    room   = req.params.room
    data   = if req.body.payload? then JSON.parse req.body.payload else req.body
    secret = data.secret

    robot.messageRoom room, "I have a secret: #{secret}"

    res.send 'OK'


それらを curl でテストしてください:後述の "エラー処理"節 も参照してください。

// JSONデータを送る場合、"Content-Type: application/json"を指定する必要があります
curl -X POST -H "Content-Type: application/json" -d '{"secret":"C-TECH Astronomy"}' http://127.0.0.1:8080/hubot/chatsecrets/general

// デフォルト(Content-Type: application/x-www-form-urlencoded)の場合、"payload=..."をセットする必要があります
curl -d 'payload=%7B%22secret%22%3A%22C-TECH+Astronomy%22%7D' http://127.0.0.1:8080/hubot/chatsecrets/general

すべてのエンドポイントURLは、(ロボットの名前に関係なく)リテラル文字列/hubotで始まる必要があります。 この一貫性により、ウェブフック(コピー可能なURL)を簡単に設定でき、URLが有効であることが保証されます(すべてのボット名がURLセーフではありません)。

イベント


Hubotはスクリプト間でデータを渡すために使用できるイベントに応答することもできます。 これは、node.js の EventEmitterrobot.emitrobot.on でカプセル化することによって行われます。

このためのユースケースの1つは、サービスとのやりとりを処理し、イベントが発生したときにイベントを発行するためのスクリプトを1つ持つことです。 たとえば、GitHubのポスト・コミット・フックからデータを受け取り、それが入ってきたときにコミットを発行し、そのコミットに対して別のスクリプトを実行させるスクリプトを作成できます。
# src/scripts/github-commits.coffee
module.exports = (robot) ->
  robot.router.post "/hubot/gh-commits", (req, res) ->
    robot.emit "commit", {
        user    : {}, #hubot user object
        repo    : 'https://github.com/github/hubot',
        hash  : '2e1951c089bd865839328592ff673d2f08153643'
    }

# src/scripts/heroku.coffee
module.exports = (robot) ->
  robot.on "commit", (commit) ->
    robot.send commit.user, "Will now deploy #{commit.hash} from #{commit.repo}!"
    #デプロイコードはここに記述

イベントを提供する場合は、データにHubot ユーザまたは hubot ルームオブジェクトを含めることを強くお勧めします。 これにより、hubot はチャット経由でユーザまたはルームに通知することができます。

エラー処理


完全なコードは存在しません、エラーと例外がかならず予想されます。 以前はキャッチされなかった例外がhubotインスタンスをクラッシュさせていました。そこで Hubot に uncaughtException ハンドラが追加されました。このハンドラは、スクリプトが例外に関する何かを行うためのフックする機能を提供します。

# src/scripts/does-not-compute.coffee
module.exports = (robot) ->
  robot.error (err, res) ->
    robot.logger.error "DOES NOT COMPUTE"

    if res?
      res.reply "DOES NOT COMPUTE"

ここで必要なことは何でも行うことができます、特に非同期コードでは、レスキューおよびロギングのエラーを予防する必要があります。 そうしないと、再帰的なエラーが発生し、何が起こっているのか分からないことがあります。

hoodの下では、エラーハンドラがそのイベントを消費する「エラー」イベントが発生しています。 uncaughtException ハンドラは、 技術的にプロセスを未知の状態のままにします 。 したがって、可能な限り、自分の例外を救済し、自分で放出する必要があります。 最初の引数は出力されたエラーで、2番目の引数はエラーを生成したオプションのメッセージです。

前のサンプルを使うと:

  robot.router.post '/hubot/chatsecrets/:room', (req, res) ->
    room = req.params.room
    data = null
    try
      data = JSON.parse req.body.payload
    catch err
      robot.emit 'error', err

    # 残りのコードをここに記述


  robot.hear /midnight train/i, (res)
    robot.http("https://midnight-train")
      .get() (err, res, body) ->
        if err
          res.reply "Had problems taking the midnight train"
          robot.emit 'error', err, res
          return
        # 残りのコードをここに記述


2番目の例では、ユーザがどのようなメッセージを表示するか考えておく必要があります。ユーザに返信するエラーハンドラがある場合は、カスタムメッセージを追加する必要はなく、 get() リクエストに提供されたエラーメッセージを返信することもできますが、もちろん例外レポートをどのように公開したいかによって異なります。

スクリプトの文書化


Hubotスクリプトは、ファイルの先頭にコメントを記述することができます。たとえば、次のようになります。

# Description:
#   <description of the scripts functionality>
#
# Dependencies:
#   "<module name>": "<module version>"
#
# Configuration:
#   LIST_OF_ENV_VARS_TO_SET
#
# Commands:
#   hubot <trigger> - <what the respond trigger does>
#   <trigger> - <what the hear trigger does>
#
# Notes:
#   &ltoptional notes required for the script>
#
# Author:
#   <github username of the original script author>

これらの中で最も重要でユーザが直面するのは Commands です。 読み込み時に、Hubot は各スクリプトの Commands セクションを見て、すべてのコマンドのリストを作成します。 含まれている help.coffee を使用すると、ユーザはすべてのコマンドまたは検索でヘルプを要求できます。 したがって、コマンドを文書化することで、ユーザはより多くの情報を発見することができます。

コマンドを文書化する際には、以下のベストプラクティスがあります:
  • 1行にとどめます。ヘルプコマンドがソートされるので、二番目の行が予期しない場所に挿入され、おそらく意味をなさなくなります。
  • hubot にが何か他の名前を付けられていても、Hubot を hubot として参照してください。自動的に正しい名前に置き換えられます。これにより、ドキュメントを更新しなくても簡単にスクリプトを共有できます。
  • robot.respond のドキュメントでは、常に接頭辞として hubot を付けます。 Hubot はこれをあなたのロボットの名前に自動的に置き換えます。ロボットの名前があればそれをエイリアスに置き換えます。
  • マニュアルページがどのように文書化されているか確認してください。特に角カッコは省略可能な部分を示し、任意の数の引数に対して '...'を指定します。

その他のセクションは、ボットの開発者、特定の依存関係、設定変数、および注意事項に関連しています。 hubot-scripts へ寄稿するには、スクリプトの起動と実行に関連するすべてのセクションが含まれている必要があります。

永続性


Hubot には、 robot.brain として公開されたインメモリの Key-Value ストアがあり、スクリプトでデータを格納および取得するために使うことができます。
robot.respond /have a soda/i, (res) ->
  # ソーダ数を取得する(数値に強制)
  sodasHad = robot.brain.get('totalSodas') * 1 or 0

  if sodasHad > 4
    res.reply "I'm too fizzy.."

  else
    res.reply 'Sure!'

    robot.brain.set 'totalSodas', sodasHad+1
robot.respond /sleep it off/i, (res) ->
  robot.brain.set 'totalSodas', 0
  msg.reply 'zzzzz'

スクリプトがユーザデータを検索する必要がある場合、 user.NameuserForIduserForFuzzyName 、および usersForFuzzyName というID、名前、または「あいまいな」一致で1つまたは複数のユーザを検索するためのrobot.brainのメソッドがあります。
module.exports = (robot) ->

  robot.respond /who is @?([\w .\-]+)\?*$/i, (res) ->
    name = res.match[1].trim()

    users = robot.brain.usersForFuzzyName(name)
    if users.length is 1
      user = users[0]
      # 何か面白いことをここに..

      res.send "#{name} is user - #{user}"

スクリプトのロード

主に3つのソースからスクリプトがロードされます。
  • scripts/ ディレクトリにある hubot インストールに バンドルされているすべてのスクリプト
    hubot-scripts.json で指定した hubot-scripts npm パッケージに含まれている コミュニティスクリプト
  • external-scripts.json で指定された外部の npm パッケージ からロードされるスクリプト

scripts/ ディレクトリから読み込まれたスクリプトは、アルファベット順にロードされるので、スクリプトの一貫したロード順序が期待できます。 例えば:
  • scripts/1-first.coffee
  • scripts/_second.coffee
  • scripts/third.coffee

スクリプトの共有


ロボットの友人の能力を拡張するためのスクリプトをいくつか作成したら、それらを世界と共有することを検討する必要があります。最低限、スクリプトをパッケージ化して Node.js パッケージレジストリ に提出すできです。以下のスクリプトを共有化するためのベストプラクティスについても検討してください。

スクリプトパッケージの作成


Hubot 用のスクリプトパッケージを作成することは非常に簡単です。最初に hobot yeoman ジェネレータをインストールします:

% npm install -g yo generator-hubot

hubot ジェネレータをインストールします。 Hubot スクリプトを作成することは新しいHubotを作成することに似ています。独自のhubotスクリプト用のディレクトリを作成し、それに新しい hubotスクリプト を生成します。 たとえば、 "my-awesome-script" という Hubot
スクリプトを作成する場合は、次のようにします:

% mkdir hubot-my-awesome-script
% cd hubot-my-awesome-script
% yo hubot:script


この時点で、スクリプトの著者、スクリプトの名前(ディレクトリ名で推測される)、簡単な説明、およびキーワード (少なくとも hubot 、このリストの hubot-scripts ) を見つけるための質問が表示されます。

git を使用している場合、生成されたディレクトリには .gitignore が含まれているため、すべてを初期化して追加できます:

% git init
% git add .
% git commit -m "Initial commit"


これで、準備が整った hubot スクリプトリポジトリが完成しました。 あらかじめ作成されている src/awesome-script.coffee ファイルを開いて、スクリプトを作成してください。 準備が整ったら、この文書 にしたがって npmjs に公開することができます!

おそらく、新しいスクリプト用の単体テストを書くことになります。 サンプルのテストスクリプトは、 test/awesome-script-test.coffee に書き込まれます。これは grunt で実行できます。 テストの詳細については、 "Hubot スクリプトのテスト"節を参照してください。

Listenerメタデータ


正規表現とコールバックに加え、 hear 関数と respond 関数もまた任意のメタデータを生成された Listener オブジェクトへ結びつけることができるオプション options Object を受け入れます。このメタデータにより、スクリプトパッケージを変更せずにスクリプトの動作を簡単に拡張することができます。

最も重要で最も一般的なメタデータキーは @id@ です。 すべてのリスナには一意の名前を付ける必要があります ( options.id ; デフォルトは null )。 名前はモジュール (たとえば 'my-module.my-listener')でスコープ化する必要があります。これらの名前は、他のスクリプトが個々のリスナに直接指定し、許可やレート制限のような追加機能を使ってそれらを拡張することを可能にします。

拡張に加えて、追加のメタデータキーを定義し、処理することができます。 詳細については、"Listener ミドルウェア" 節を参照してください。

前の例に戻ります:

module.exports = (robot) ->
  robot.respond /annoy me/, id:'annoyance.start', (msg)
    # 誰かを怒らせるコード

  robot.respond /unannoy me/, id:'annoyance.stop', (msg)
    # 迷惑をかけないようにするコード

これらのスコープ付き識別子を使用すると、次のような新しい動作を外部から指定できます:
  • 権限ポリシ: " annoyers グループの誰もが annoyance.* コマンドを許可する"
  • レート制限:"30分ごとに1回 annoyance.start の実行のみを許可する"

ミドルウェア


ミドルウェアには、Receive (受信)、Listener (リスナ)、Response (応答)の3種類があります。

Receive (受信)ミドルウェアは、リスナーがチェックされる前に1回だけ実行します。
Listener (リスナ)ミドルウェアは、メッセージに一致するすべてのリスナに対して実行します。
Response (応答)ミドルウェアは、メッセージに送信されるすべての応答に対して実行します。

プロセス実行およびAPI


Expressミドルウェアと同様に、Hubot はミドルウェアを定義順に実行します。各ミドルウェアは、( nextを呼ぶことで)チェーンを続行するか、( doneを呼ぶことで)チェーンを中断するかどちらかが可能です。すべてのミドルウェアが継続すると、リスナコールバックが実行され、 done が呼び出されます。ミドルウェアは done コールバックをラップして、(リスナコールバックが実行されたか、ミドルウェアのより深い部分が中断された後)プロセスの後半でコードを実行を許可します。

ミドルウェアは次のように呼び出されます:

context
  • 各ミドルウェアタイプのAPIを参照して、コンテキストが公開する内容を確認
next 
  • プロパティを持たないFunctionで、次のミドルウェアを継続/Listerコールバック実行
  • next が(done 関数か最終的に done がコールされる新たな関数のどちらかを提供するか)を指定する単一のオプション引数付きで呼び出される
  • もし引数が与えられなかったならば、提供された done が責任を負う

done
  • ミドルウェアの実行を中断し、一連の完了関数の実行を開始するために呼び出される追加のプロパティを持たないFunction引数無しで done が呼び出される

すべてのミドルウェアは、 context next 、および done の同一のAPIシグニチャを受け取ります。異なる種類のミドルウェアは、 context オブジェクト内で異なる情報を受け取ることがあります。 詳細は、各タイプのミドルウェアのAPIを参照してください。

エラーハンドリング


(イベントループを生成しない)同期ミドルウェアの場合、標準リスナの場合と同様に、自動的にエラーを検出してエラーイベントを生成します。 Hubot は自動的に最新の完了コールバックを呼び出してミドルウェアスタックを巻き戻します。非同期ミドルウェアは、独自の例外をキャッチし、エラーイベントを発行し、完了を呼び出す必要があります。 キャッチされない例外は、ミドルウェア完了コールバックのすべての実行を中断します。

Listener ミドルウェア


Listener ミドルウェアは、メッセージと一致するリスナと実行中のリスナとの間にロジックを挿入します。これにより、一致するスクリプトごとに実行される拡張機能を作成できます。例には、集中型の認可ポリシ、レート制限、ロギング、およびメトリックが含まれます。ミドルウェアは、他の hubot スクリプトと同様に実装されています。 hear メソッドや respondメソッドを使用する代わりに、ミドルウェアは listenerMiddleware を使用して登録されます。

Lister ミドルウェアの例

完全に機能する例は、 hubot-rate-limit にあります。

ミドルウェアのロギングコマンド実行の簡単な例:

module.exports = (robot) ->
  robot.listenerMiddleware (context, next, done) ->
    # ログコマンド
    robot.logger.info "#{context.response.message.user.name} asked me to #{context.response.message.text}"
    # ミドルウェアの実行を継続
    next()

この例では、リスナに一致するチャットメッセージごとにログメッセージが書き込まれます。

レート制限の決定を行うもっと複雑な例:

module.exports = (robot) ->
  # 最後の実行時にリスナIDをマップ
  lastExecutedTime = {}

  robot.listenerMiddleware (context, next, done) ->
    try
      # リスナが異なる最小期間を指定しない限り、デフォルトは1秒です
      minPeriodMs = context.listener.options?.rateLimits?.minPeriodMs? or 1000

      # コマンドが最近実行されたかどうかを確認
      if lastExecutedTime.hasOwnProperty(context.listener.options.id) and
         lastExecutedTime[context.listener.options.id] > Date.now() - minPeriodMs
        # コマンドがとても早く実行されています!
        done()
      else
        next ->
          lastExecutedTime[context.listener.options.id] = Date.now()
          done()
    catch err
      robot.emit('error', err, context.response)

この例では、ミドルウェアがリスナが最後の1,000msで実行されたかどうかを確認します。存在する場合、ミドルウェアの呼び出しはすぐに完了し、リスナのコールバックが呼び出されないようにします。リスナの実行が許可されている場合、ミドルウェアは完了したハンドラーをアタッチし、リスナーが実行を終了した時刻を記録できるようにします。

またこの例では、リスナ固有のメタデータを使用して非常に強力な拡張機能を作成する方法も示しています:スクリプト開発者がレート制限ミドルウェアに対してミドルウェアを追加しリスナオプションを設定するだけで異なるレートへのより簡単なレート制限コマンドを簡単に使うことができます。

module.exports = (robot) ->
  robot.hear /hello/, id: 'my-hello', rateLimits: {minPeriodMs: 10000}, (msg) ->
    # 10秒に1回以上実行されません
    msg.reply 'Why, hello there!'

Lister ミドルウェア API



リスナのミドルウェアコールバックには、 context next 、そして done という3つの引数があります。 next done については、 ミドルウェアAPIを参照してください。 リスナのミドルウェア・コンテキストには、次のフィールドがあります。
lister
  • options : リスナ定義時のオプションセットを含む単純なObject。 Listenerメタデータ節 を参照のこと。
  • その他全てのプロパティは内部で考慮される。

response
  • 標準レスポンスAPIのすべての部分がミドルウェアAPIに含まれている。 sendおよびreply節を参照のこと。
  • ミドルウェアは応答オブジェクトを追加情報でdecorate (ただしmodifyできない)ことが可能(例:ユーザのLDAPグループを用いて response.message.user にプロパティ追加)
  • 注:テキストメッセージ( response.message.text ) は、リスナミドルウェアで不変であるとみなされるべき

Receive ミドルウェア


Receive (受信) ミドルウェアは、リスナが実行される前に実行されます。 ID、metrics などを追加するように更新されていないコマンドをブラックリストに登録するのに適しています。

Receive ミドルウェアの例


このシンプルなミドルウェアは、 hear リスナを含む特定のユーザによる使用を禁止します。 ユーザが明示的にコマンドを実行しようとすると、エラーメッセージが返されます。

BLACKLISTED_USERS = [
  '12345' # 請負業者のユーザーIDのアクセスを制限
]

robot.receiveMiddleware (context, next, done) ->
  if context.response.message.user.id in BLACKLISTED_USERS
    # このメッセージをこれ以上処理しないこと。
    context.response.message.finish()

    # メッセージが 'hubot'またはエイリアスパターンで始まる場合、
    # このユーザは明示的にコマンドを実行しようとしていたため、
    # エラーメッセージで応答します。
    if context.response.message.text?.match(robot.respondPattern(''))
      context.response.reply "I'm sorry @#{context.response.message.user.name}, but I'm configured to ignore your commands."

    # これ以上ミドルウェアを実行しない。
    done()
  else
    next(done)

Receive ミドルウェア API


Receive ミドルウェアのコールバックは、 context next 、および done という3つの引数を受け取ります。  next done については、 "ミドルウェアAPI":節を参照してください。 Receive ミドルウェア context には、次のフィールドがあります:
response
  • このレスポンスオブジェクトには match プロパティーがない。これはまだ match するリスナがないためである。
  • ミドルウェアは、Receive オブジェクトを追加情報で装飾してもよい(例:ユーザの LDAP グループを用いて response.message.user にプロパティを追加)。
  • ミドルウェアは response.message オブジェクトを変更することがある。

Resonse ミドルウェア


Resonse (応答) ミドルウェアは、 hubot がチャットルームに送信するすべてのメッセージに対して実行されます。 メッセージのフォーマット、パスワードの漏洩、metrics などに役立ちます。

Resonse ミドルウェアの例


この簡単な例では、チャットルームに送信されるリンクの形式を、マークダウンリンク(たとえば " example ":https://example.com/ )から "Slack": でサポートされる形式 ( https://example.com|example ) に変更します。
module.exports = (robot) ->
  robot.responseMiddleware (context, next, done) ->
    return unless context.plaintext?
    context.strings = (string.replace(/\[([^\[\]]*?)\]\((https?:\/\/.*?)\)/, "<$2|$1>") for string in context.strings)
    next()

Resonse ミドルウェア API


Resonse ミドルウェアのコールバックは、 context 、 next 、そして done の3つの引数を受け取ります。  next と done については、 "ミドルウェアAPI":節を参照してください。 Resonse ミドルウェアcontextには、次のフィールドがあります:
response
  • このresponseオブジェクトは、ミドルウェアから新しいメッセージを送信するために使用可能である。 これらの新しい応答に対してミドルウェアが呼び出される。無限ループを作成しないように注意すること。

strings
  • チャットルームアダプタに送信される文字列の配列。 これらの編集や、 context.strings = ["new strings"] を使った置換が可能。

method
  • リスナが送信したresponseメッセージのタイプ( send reply emote topic など)を表す文字列。

plaintext
  • true または undefined 。 メッセージが通常の平文タイプ(送信や返信など)の場合、これは true に設定される。このプロパティは読み取り専用として扱う必要がある。

Hubot スクリプトのテスト


hubot-test-helperは、Hubot スクリプトの単体テストに適したフレームワークです ( hubot-test-helper を使用するには、 Promises をサポートする最近の Node バージョンが必要となります)。

Hubot インスタンスにパッケージをインストールします:

% npm install hubot-test-helper --save-dev


また、次のものをインストールする必要があります:
  • Mocha などの JavaScript テストフレームワーク
  • chai expect.js などのアサーションライブラリ

また、次のものもインストールしたくなるでしょう:
  • coffee-script (JavaScriptではなくCoffeeScriptでテストを書く場合)
  • Sinon.js のような mock ライブラリ(スクリプトがWebサービス呼び出しやその他の非同期アクションを実行する場合)

ここでは、 Hubot サンプルスクリプト の最初の2つのコマンドをテストするサンプルスクリプトを示します。 このスクリプトでは、 Mochachaicoffee-script 、そしてもちろん hubot-test-helper を使用しています:
test/example-test.coffee
Helper = require('hubot-test-helper')
chai = require 'chai'

expect = chai.expect

helper = new Helper('../scripts/example.coffee')

describe 'example script', ->
  beforeEach ->
    @room = helper.createRoom()

  afterEach ->
    @room.destroy()

  it 'doesn\'t need badgers', ->
    @room.user.say('alice', 'did someone call for a badger?').then =>
      expect(@room.messages).to.eql [
        ['alice', 'did someone call for a badger?']
        ['hubot', 'Badgers? BADGERS? WE DON\'T NEED NO STINKIN BADGERS']
      ]

  it 'won\'t open the pod bay doors', ->
    @room.user.say('bob', '@hubot open the pod bay doors').then =>
      expect(@room.messages).to.eql [
        ['bob', '@hubot open the pod bay doors']
        ['hubot', '@bob I\'m afraid I can\'t let you do that.']
      ]

  it 'will open the dutch doors', ->
    @room.user.say('bob', '@hubot open the dutch doors').then =>
      expect(@room.messages).to.eql [
        ['bob', '@hubot open the dutch doors']
        ['hubot', '@bob Opening dutch doors']
      ]
サンプル出力
% mocha --compilers "coffee:coffee-script/register" test/*.coffee


  example script
    ✓ doesn't need badgers
    ✓ won't open the pod bay doors
    ✓ will open the dutch doors


  3 passing (212ms)

以上
----------

この文書を読めば、

  • エラーハンドリングの仕方
  • ロードされる順番
  • コメントもHubothはチェックしている件(特にCommands)
  • タイムアウトやインターバルで遅い場合の応答も可能
  • 永続性ならNoSQL robot.brain
  • N回に1回実行するパターン
  • Listener、Receiveミドルウェア
  • hubot-test-helper およびテストフレームワークの活用によるテスト

といったところが理解できる。


p.s.
以下の記事も書きました。
よろしければ、どうぞ。

Hubot Document: Patterns を翻訳して、チャットボットでありがちな実装を確認する
https://fight-tsk.blogspot.jp/2017/04/hubot-document-patterns.html

0 件のコメント:

o1-previewにナップサック問題を解かせてみた

Azure環境上にあるo1-previewを使って、以下のナップサック問題を解かせてみました。   ナップサック問題とは、ナップサックにものを入れるときどれを何個入れればいいかを計算する問題です。数学では数理最適化手法を使う際の例でよく出てきます。 Azure OpenAI Se...