Backlog」タグアーカイブ

Backlog + Chatwork + Sinatraで課題の更新通知を行う

前提

以下の環境で実装、動作確認

要素 バージョン
debian 8.6
ruby 2.2.2
sinatra 1.4.7

概要

BacklogのWebhook機能を用いて、Sinatraで実装したWebアプリケーションを経由し、チャットワークにリアルタイム更新通知を行ったお話。

メール通知より圧倒的に早いリアルタイム通知による、プロジェクト管理の効率化ができる可能性を探る。

今回は試験的実装のため、課題作成時に課題の作成者、タイトル、URLをチャットワークのマイチャットに投稿するだけの簡単なシステムを実装した。

なお、今回は非常に軽量なシステムとなるので、Rubyの軽量WebフレームワークであるSinatraを採用しているが、Sinatraについては本記事から脱線するので詳しい解説は割愛する。

Webhookとは

端的に言えばアプリケーションで何らかの更新アクションが発生した時、その内容を特定のURLに対してPOSTしてくれる機能。
Backlogの場合、課題が追加された、更新された、削除されたなどの情報を、指定したURLに対してJSONでPOSTさせることができる。
技術とかツールでなく、ただの機能の名称なので注意。

参考 Webhookとは? on @Qiita

Webhookの設定を行う

BacklogでのWebhookの設定はプロジェクト単位で行う。
プロジェクト設定に、Webhookの項目があるので、そこから設定する。

Webhook名と説明は適当に入力し、WebHookURLに更新通知をPOSTさせたいURLを入力する。当然、インターネットからアクセスできる場所でないといけないので、ローカル開発環境などを指定しても動作しない。

「通知するイベント」は、Backlogのどの更新アクションをWebhookで通知するかを指定できる。今回は課題の追加時にチャットワークで通知したいので、「課題の追加」にのみチェックを入れる

Webhookの受け口作成(Sinatra)

本記事では、Webhookの受け口に、Rubyの軽量WebアプリケーションフレームワークであるSinatraを用いる。といっても、POSTされたデータをチャットワークに流すだけなので、フレームワークも不要なレベルだが、導入が簡単なので今回はSinatraを用いることにした。

なお、Sinatra環境の構築には、手前味噌だが以下を用いた。
Sa2Knight/Sinatra-Skeleton: Sinatraアプリケーションを最短で構築する個人用リポジトリ

Backlogの更新内容を出力

Sinatraの詳細的な説明は割愛するが、以下がWebhookの受け口となるエンドポイントの実装である

require 'sinatra/base'
require 'json'
require 'pp'

class App < Sinatra::Base

  post '/' do
    pp JSON.parse request.body.read
    return true
  end

end

BacklogのWebhookでは、指定したURLに対して、リクエストボディがJSONのPOSTリクエストが飛んで来る。
上記コードでは、それを受け取ってJSONをparseし、標準出力している。

サーバを立ち上げてから、以下のような課題を作成すると

WebhookによってJSONがPOSTされ、以下のような標準出力が得られる

{"created"=>"2017-08-15T13:44:56Z",
 "project"=>
  {"archived"=>false,
   "projectKey"=>"DEV",
   "name"=>"個人開発",
   "id"=>38382,
   "subtaskingEnabled"=>false},
 "id"=>19571304,
 "type"=>1,
 "content"=>
  {"summary"=>"課題タイトル",
   "key_id"=>275,
   "customFields"=>[],
   "dueDate"=>"2017-08-09",
   "description"=>"課題詳細",
   "priority"=>{"name"=>"中", "id"=>3},
   "resolution"=>{"name"=>"", "id"=>nil},
   "actualHours"=>nil,
   "issueType"=>
    {"color"=>"#7ea800",
     "name"=>"タスク",
     "displayOrder"=>0,
     "id"=>172585,
     "projectId"=>38382},
   "milestone"=>[],
   "versions"=>[],
   "parentIssueId"=>nil,
   "estimatedHours"=>nil,
   "id"=>2889963,
   "assignee"=>
    {"name"=>"sa2knight",
     "id"=>85748,
     "roleType"=>255,
     "lang"=>"null",
     "userId"=>"sa2knight"},
   "category"=>[{"name"=>"開発関係", "displayOrder"=>0, "id"=>79173}],
   "startDate"=>"",
   "status"=>{"name"=>"未対応", "id"=>1}},
 "notifications"=>[],
 "createdUser"=>
  {"nulabAccount"=>nil,
   "name"=>"sa2knight",
   "mailAddress"=>nil,
   "id"=>85748,
   "roleType"=>1,
   "userId"=>nil}}

フォーマットはBacklog APIと概ね一緒なのでここでは割愛する。
BacklogAPIについては[Ruby] BacklogをCUIで操作してみる | QS-DEVSでも触れている。

通知に必要な情報のみ抜き出す

受け取ったデータのうち、今回必要なのは以下の3種類

  • 課題キー
  • 課題名
  • 課題作成者

以下のようにコードを修正して、必要な情報のみ抜き出す。

  post '/' do
    params = JSON.parse request.body.read
    @issue = {
      key:     "#{params['project']['projectKey']}-#{params['content']['key_id']}",
      summary: params['content']['summary'],
      creator: params['createdUser']['name'],
    }
    pp @issue
    return true
  end

これで出力は以下のようになる

{:id=>"DEV-137", :summary=>"課題タイトル", :creator=>"sa2knight"}

チャットワーク連携の準備

次に、チャットワーク連携用のロジックを用意する。
こちらについても手前味噌だが、RubyでチャットワークAPIを利用するクラスを以前作っていたので、それをベースにする。
Sa2Knight/chatwork-ruby: RubyでチャットワークAPI呼んで遊ぶ

チャットワークAPIについては、以下参照
チャットワークAPIをRubyで利用する | QS-DEVS

今回はAPIの認証と、メッセージの送信ができればいいので、大幅にコードを削って以下のようなクラスに仕上げた。

require 'net/http'
require 'uri'
require 'json'
require 'date'

class Chatwork

  @@API_BASE = 'https://api.chatwork.com/v2'
  @@ROOM_ID  = 'hogehogefugafuga'

  # tokenを指定してオブジェクトを生成
  # tokenを省略した場合、環境変数を参照する
  def initialize(token = nil)
    @token = token || ENV['CHATWORKAPI']
  end

  # ルームに新規メッセージを送信
  # room_id: 対象のroomID
  # body:    投稿する本文
  def sendMessage(body)
    url = '/rooms/' + @@ROOM_ID + '/messages'
    res = createHttpObject(url, :post, {:body => body})
    return res.body ? JSON.parse(res.body) : []
  end

  private
    # HTTPリクエストを送信する
    def createHttpObject(url, method, params = {})
      api_uri = URI.parse(@@API_BASE + url)
      https = Net::HTTP.new(api_uri.host, api_uri.port)
      https.use_ssl = true
      api_uri.query = URI.encode_www_form(params) if method == :get
      req = createRequestObject(method, api_uri)
      req["X-ChatWorkToken"] = @token
      req.set_form_data(params) unless method == :get
      https.request(req)
    end
    # リクエストオブジェクトを生成する
    def createRequestObject(method, uri)
      case method
        when :get
          return Net::HTTP::Get.new(uri.request_uri)
        when :post
          return Net::HTTP::Post.new(uri.request_uri)
        when :put
          return Net::HTTP::Put.new(uri.request_uri)
        when :delete
          return Net::HTTP::Delete.new(uri.request_uri)
      end
    end
end

環境変数”CHATWORKAPI”にAPIキーを設定し、このクラスのインスタンスを生成後、sendMessageメソッドを呼び出せばチャットワークにメッセージを送信できる。

なお、今回はメッセージの送信先として、マイチャットを指定している。

課題の更新を通知する

前項で実装したChatworkクラスを用いて、Webhookで送られた更新情報をチャットワークに通知する。
Sinatraの使い方としてはかなりお行儀が悪いが、プライベートメソッドmakeChatworkMessageを実装し、チャットワークに送信するメッセージを作成、それをチャットワークに送信するようにした。

require 'sinatra/base'
require 'json'
require 'pp'
require_relative 'chatwork'

class App < Sinatra::Base

  post '/' do
    params = JSON.parse request.body.read
    issue = {
      key:     "#{params['project']['projectKey']}-#{params['content']['key_id']}",
      summary: params['content']['summary'],
      creator: params['createdUser']['name'],
    }
    Chatwork.new.sendMessage(makeChatworkMessage(issue))
    return true
  end

  private
  def makeChatworkMessage(issue)
    message =  "[info][title]#{issue[:creator]}さんが課題を作成しました[/title]"
    message += "[#{issue[:key]}] #{issue[:summary]}\n"
    message += "https://saknight.backlog.jp/view/#{issue[:key]}[/info]"
  end

end

動作確認

サーバを起動し、課題の作成を行うと、チャットワークのマイチャットにリアルタイムで通知が届くことが確認できた。

所感

  • 初めてWebhookを使ったが、かなりリアルタイムに近い。APIでpull型で取得するよりもコードもスマートに書けて良い
  • メール通知より遥かに確実に、高速に通知できるので使いみちはあると思う
  • BacklogWebhookの仕様上、インターネットで繋がる場所にサーバを建てる必要があるため、セキュリティにちょっと気をつける必要ありそう
  • 以下のような工夫ができる余地がある
    — プロジェクトごとに通知するチャットルームを切り替える
    — 担当者に対応するユーザに対してToを付ける
    — 課題のクローズ時にも作成者にToを付けて通知する

[Ruby] BacklogをCUIで操作してみる

前提

要素 バージョン
Debian 8.6
Ruby 2.2.2
gem 2.4.5
bundle 1.13.4
Backlog API 2.9.0
BacklogKit 0.15.0

また、Backlogの個人設定より、APIキーを生成していることを前提とする

概要

国産プロジェクト管理ツールであるBacklogを、ブラウザではなく、BacklogAPIを用いて、スクリプト経由で操作する。スクリプトの記述には、APIを間接的に利用するライブラリであるBacklogKitを用いるためにRubyを採用する。

本記事で作成するツールでは、主に以下のことをCUIで実現する

  • ユーザ情報の取得
  • 課題の作成(タイトル/本文のみ指定可能)
  • 課題の操作(ステータスの変更/完了理由の変更/実作業時間の変更)

実用的にするにはさらにカスタマイズする必要があるが、本記事では基本的なAPI利用が出来るようになる部分までにする。また、本課題での動作確認は、個人利用のために開設しているBacklogスペースを用いる。

BacklogAPI利用の準備

今回は、BacklogAPIをRubyで簡易的に利用するためのライブラリである、BacklogKitを利用する。BacklogKitはgemで管理されているので、bundleを用いてインストールする。

※ gem: Rubyのパッケージングシステム
※ Bundle: gemパッケージを管理するツール

Gemfileに以下のように記述

source "https://rubygems.org"
gem 'backlog_kit'

Bundleでインストール

bundle install --path bundle/vendor

スペースIDとAPIキーを記述したファイルを作成

BacklogAPIを利用するためには、対象のスペースIDと、APIキーが必要になる。スクリプト中にハードコーディングしても良いが、本記事で実装するツールは、Githubにてコードを公開しているため、APIキーが漏洩することを避けるために別途JSONファイルに記述することにする(もちろん該当のJSONファイルはリポジトリで共有しない)

ということで、以下のようなsecret.jsonを作成する。もちろんAPI_KEYはダミー

{
  "space_id": "saknight",
  "api_key": "hogehogefugafugafoofoobarbar",
  "project_key": "DEV"
}

とりあえず汎用的なメソッドを持たせることを目的に、Util.rbを作成し、そこにJSONファイルをロードするメソッドを実装する

require 'json'
class Util
  def self.load_secret_file
    File.open('secret.json') do |file|
      JSON.load(file)
    end
  end
end

これでBacklogAPIを利用する下準備は完了

BacklogAPIを使ってみる

Backloger.rbを作成し、ここにBacklogAPIを利用するコードを書いていく。とりあえずBacklogAPIを試しに使ってみるということで、ユーザ情報を取得してみる。

require 'backlog_kit'
require_relative 'Util'

secret = Util.load_secret_file
client = BacklogKit::Client.new(
  space_id: secret['space_id'],
  api_key: secret['api_key']
)

p client.get_space.body

bundleを使っているため、上記ファイルを以下のように実行する

$ bundle exec ruby Backloger.rb

以下のような実行結果が得られた

$ bundle exec ruby Backloger.rb
#<BacklogKit::Resource:0x007f9534806850 @attributes={:spaceKey=>"saknight", :name=>"saknight", :ownerId=>85748, :lang=>"ja", :timezone=>"Asia/Tokyo", :reportSendTime=>"18:00:00", :textFormattingRule=>"backlog", :created=>"2017-03-04T15:47:40Z", :updated=>"2017-03-04T15:47:40Z"}>

どうやらBacklogKitではAPIの実行結果はBacklogKit::Resourceクラスのオブジェクトとして返却されるらしい。詳しい使い方はドキュメントやソースコードを追う必要があるが、以下のようにコードを変更すればスペースIDが取得できそうだ。

p client.get_space.body.spaceKey

実行結果

$ bundle exec ruby Backloger.rb
"saknight"

とりあえずBacklogkitを用いたBacklogAPIの利用が正常にできていることがわかった。あとはドキュメントやソースコードを読んで本ツールの目的を達成するためのコードを作成していく

課題の作成に必要なパラメータの取得

リファレンスによると、課題の追加には以下のパラメータが必須となる

  • 課題の名前
  • プロジェクトID
  • 課題の種別ID
  • 課題の優先度ID

ここで面倒なのが、プロジェクト/種別/優先度が全てIDを要求されていることだ。
例えば優先度の場合「高」「中」「低」といった表示名で指定するのではなく、それに対応するIDが必要になる。面倒なことにIDはBacklog環境のカスタマイズ次第で変わってしまうので、今回はスクリプトから動的に取得できるようにすることにする。

プロジェクトID/種別ID/優先度IDを動的に取得するために、以下のIdentifierクラスを作成した

require 'backlog_kit'

class Identifier

  def initialize(client)
    @client = client
  end

  def project(projectKey)
    project = @client.get_projects.body.find do |pb|
      pb.projectKey == projectKey
    end
    project.id
  end

  def priority(label)
    priority = @client.get_priorities.body.find do |pr|
      pr.name == label
    end
    priority.id
  end

  def issueType(project_key, label)
    type = @client.get_issue_types(project_key).body.find do |it|
      it.name == label
    end
    type.id
  end
end

上記クラスのメソッドは、それぞれプロジェクトキー、優先度、種別を指定すると、それに対応するIDを返却してくれる。
例えば以下のようなコードを実行すると

identifier = Identifier.new(client)
p identifier.projectId("DEV")
p identifier.priorities('中')
p identifier.issueTypeId("DEV", 'タスク')

以下のようにそれぞれIDに変換して出力してくれる

$ bundle exec ruby Backloger.rb
38382
3
172585

もちろんIDは基本的に不変であるため、毎回動的に取得する必要はなく、一度取得したら適当にキャッシュするなどすれば簡単に高速化することができるが、ここでは割愛する。

課題の作成

ようやく本題。前項で、課題の作成に必要なパラメータを動的に取得できるようになったので、create_issueメソッドを用いて、課題を作成する。以下のコードでは、「課題テスト」という、優先度が「中」で、種別が「タスク」の課題を作成する(他の設定項目は未設定状態)

client.create_issue('課題テスト', {
  :projectId   => identifier.project(secret['project_key']),
  :priorityId  => identifier.priority('中'),
  :issueTypeId => identifier.issueType(secret['project_key'], 'タスク'),
})

スクリプト実行後、課題が生成されていることが確認できる

課題の編集に必要なパラメータの取得

今回は、前項で作成した課題に対して、以下の編集を行う。

  • ステータスを「完了」にする
  • 完了理由を「対応済み」にする
  • 実績時間を「1時間」にする
  • 適当なコメントを投稿する

“課題の作成に必要なパラメータの取得”同様、ステータス/完了理由は、対応するIDが必要になるので、同様にIdentifierクラスに以下のメソッドを追加する。

  def status(label)
    status = @client.get_statuses.body.find do |st|
      st.name == label
    end
    status.id
  end

  def resolution(label)
    resolution = @client.get_resolutions.body.find do |res|
      res.name == label
    end
    resolution.id
  end

以下のコードを実行すると

identifier = Identifier.new(client)

p identifier.status('完了')
p identifier.resolution('対応済み')

以下のように完了と対応済みに対応するIDが取得できる

$ bundle exec ruby Backloger.rb
4
0

課題の編集

前項で課題編集のためのIDが取得できたので、update_issueメソッドを用いて、以下のように課題の修正を行う

client.update_issue('DEV-47', {
  :statusId => identifier.status('完了'),
  :resolutionId => identifier.resolution('対応済み'),
})

実行すると、以下のように課題が修正されたことが確認できる

所感

  • Backlogを普段から利用してるが、どうにも定型的な操作が億劫だと思っていたので、色々自動化してみたいと思いBacklogAPIの利用を初めてみた
  • リファレンスを見る限りほぼ全ての操作をAPIで利用できるので、極端に言えばブラウザを一切使わずにBacklogを利用することもできそう
  • 本記事ではBacklogAPI及びBacklogKitについて軽く触れた程度だが、ここから実用的なツールの開発を進め、一段落したところでまた記事を書ければなと思う
  • ここ1ヶ月資格試験に追われてなかなかプログラミング出来てなかったので、リハビリがてら楽しくコーディングできた
  • Rubyを使うのも久しぶりだが、リスト操作の柔軟さはやはりRubyならでは。気持ちよく使える