こんにちは、Androidエンジニアの堀江です。最近はiOSのプロジェクトに参加してSwiftを書いています。新しいことを始めるのは楽しいですね。
ところで今ご覧になられている弊社の技術ブログ「VASILY DEVELOPERS BLOG」は、VASILYのエンジニアが交代で更新しています。記事に何を書くかは各エンジニアの裁量に任されていますが、公開前に社内でレビューをするようにしています。
レビューをする際には、以下のような点に注意しています。
- 誤字脱字・文法上の間違いが無いか
- 間違った情報が無いか
- 文章中にわかりにくい表現や解説が無いか
このうち、誤字脱字・文法上の間違いは、文章校正ツールを使うことで機械的にチェックすることが可能です。それによって、文章そのもののより本質的なレビューに時間を割くことができます。記事はレビュー前に文章校正済みであるのが理想ですが、実際には忘れる事も多いです。そのため、ある程度自動的に文章校正を実行してくれると捗ります。
本記事では、botを利用することで文章校正の実行を自動化する試みについてご紹介します。
記事公開までの流れについて
記事の公開までの流れは、概ね以下の通りになっています。
- 記事を執筆
- 記事のレビュー
- 指摘点の修正
- 公開
VASILYでは社内の情報共有にQiita:Teamを使用しており、ブログ記事についてもある程度書いた段階でQiita:Teamに投稿してしまいます。普段どのようにQiita:Teamを使っているかは、以下の記事にまとめて頂いています。
https://codeiq.jp/magazine/2017/04/50255/codeiq.jp
そして、記事の執筆が完了するとSlackのエンジニアチャンネルにレビュー依頼を投げ、指摘を受けた点について修正後、公開します。また、何時誰が記事を書くかはスケジュールを予め決めています。(歴史的経緯により社内ではDEVELOPERS BLOGの事をテックブログと呼んでいます)
どう自動化するか?
記事がQiita:Teamに投稿・更新された際に文章校正を自動で実行します。Qiita:Teamで発生する様々なイベントはWebhookで受取ることができるので、記事が投稿・更新されたタイミングで任意の処理を実行可能です。Webhookの受け取りには、Heroku上にホストしたHubotを利用します。そして、文章校正にはtextlintを使用します。
全体の流れ
全体の流れを図にすると以下のようになります。
① 記事の投稿・更新
② HubotがWebhookを受け取り記事本文を取得
③ 記事本文に対してtextlintで文章校正を実行
④ 文章校正の結果を取得
⑤ 対象の記事に文章校正の結果をコメント
⑥ 文章校正の結果を確認
②での記事本文の取得、⑤でのコメント投稿には、Qiita API v2(以下Qiita API)を使用します。
Webhookの概要と設定方法
Qiita:DeveloperのWebhooksにWebhookの仕様がまとまっています。ドキュメントより、Qiita:Teamから受け取れるイベントの一覧は以下のようになっています。
イベント名 | 概要 |
---|---|
ItemCreated | 記事が作成されたときに送信 |
ItemDestroyed | 記事が削除されたときに送信 |
ItemUpdated | 記事が更新されたときに送信 |
CommentCreated | コメントが作成されたときに送信 |
CommentDestroyed | コメントが削除されたときに送信 |
CommentUpdated | コメントが更新されたときに送信 |
MemberAdded | チームメンバーが追加されたときに送信 |
MemberRemoved | チームメンバーが離脱したときに送信 |
PingRequested | Webhookの設定画面からテストを行ったときに送信 |
ProjectCreated | プロジェクトが作成されたときに送信 |
ProjectDestroyed | プロジェクトが削除されたときに送信 |
ProjectUpdated | プロジェクトが更新されたときに送信 |
リクエスト
イベントが発生すると指定したURLにPOSTリクエストが送信されます。リクエストボディにJSON形式でエンコードされた文字列が含まれ、これをPayload
と呼びます。
また、共通リクエストヘッダとしてX-Qiita-Event-Model
にPayload
のmodel
プロパティと同様の値が、X-Qiita-Token
にWebhookに割り当てられたトークンが含まれます。
Payload
Payload
には、必ずaction
プロパティとmodel
プロパティが含まれ、action
プロパティは「どのイベントが発生したか」を表す文字列で、model
プロパティは「何に対してイベントが発生したか」を表す文字列です。この他に各イベントに関連するデータが含まれます。
例えば、記事作成を表すItemCreatedイベントの場合、Payloadは以下の様になります。その他のイベントについては公式ドキュメントを参照してください。
プロパティ名 | 型 | 説明 |
---|---|---|
action | String | "created" |
model | String | "item" |
item | Item | 作成された記事 |
user | User | 作成したユーザ |
Item
、User
型がどのようなプロパティを含むかは、公式ドキュメントのTypesから確認できます。
設定方法
Webhookのリクエスト先や、通知したいイベントの設定は、設定メニューから設定をします。通知したいイベントにチェックを付け、URLにはイベントを送信したいURLを指定します。Webhookの設定はチームの管理者のみ設定メニューに表示されます。
textlintの設定と実行
textlintはテキスト向けのLintツールで、予め定義したルールに沿って文章校正を行ってくれます。textlintの利用方法としてCLIから利用することが多いですが、今回は、Hubotのコードからモジュールとして実行します。
ここでは、textlintの設定とモジュールとしての実行方法について紹介します。ローカルで動作を確認できるようサンプルプロジェクトを用意しました。
サンプルプロジェクトのセットアップ
サンプルプロジェクトをcloneしnpm install
を実行します。Node.jsのバージョンはv6.10.2
で動作確認をしています。Node.jsをインストール済みで無い場合、nvmやnodebrew等を使用してインストールするとNode.jsのバージョンを簡単に切り替えることができます。
git clone [email protected]:horie1024/textlint-sample.git cd textlint-sample npm install
.textlintrcの設定
サンプルでは、textlintのルールとしてtextlint-ja/textlint-rule-preset-ja-technical-writingを使用しています。.textlintrc
は以下のようになります。
{ "rules": { "preset-ja-technical-writing": true } }
モジュールとしてtextlintを実行
モジュールとしてtextlintを実行するコードは以下のようになります。TextLintEngine
をインスタンス化する際にconfigFile
に.textlintrc
のパスを指定します。そして、TextLintEngineCore#executeOnText
を使用すると引数に取ったテキストに対してtextlintを実行することができます。引数にファイルを指定したい場合TextLintEngineCore#executeOnFiles
を使用します。
const TextLintEngine = require("textlint").TextLintEngine; const engine = new TextLintEngine({ configFile: "config/.textlintrc" }); engine.executeOnText("# test!!").then(results => { console.log(results[0].messages); if (engine.isErrorResults(results)) { const output = engine.formatResults(results); console.log(output); } });
実行結果は以下のように出力されます。
実装
ここまでで紹介した、Qiita:TeamのWebhookとtextlintのモジュールとしての実行を組み合わせて以下の流れを実現します。
サンプルプロジェクト
作成済みのHubotプロジェクトをサンプルプロジェクトとして用意しました。こちらをclone後rootディレクトリでnpm install
を実行することでローカルでHubotを動かせるようになります。
Node.jsのバージョンはv6.10.2
で動作確認をしています。
この記事では、Heroku上でHubotをホストするための設定方法は紹介しませんが、Web上に良い記事が多くあります。参考にしてみてください。
文章校正を実行する条件
Webhookでは、全ての記事の投稿・更新を受け取れるので、記事に対して文章校正を実行するかを判断する必要があります。Qiita APIを使用すると、タグだけでなく投稿者やコメント等を取得できますので、これらの情報を判別に用いる事も可能です。
今回は、以下の条件に当てはまる場合に文章校正を実行します。
- 記事にタグ
techblog
が付いている - WebhookのPayload中の
model
が"item"
、action
が"created"
もしくは"updated"
である
Webhookでの各種イベントの受け取り
今回はHeroku上にホストしたHubotでWebhookを受け取ります。Qiita:TeamからのWebhookはPOSTリクエストですので、HubotのHTTP Listenerを利用してイベントを受け取ります。
実際のコードは以下のようになります。このコードをHubotプロジェクトのscriptsディレクトリ以下に配置します。Webhookを受け取るためのパスは/qiita/webhooks
とします。Webhookの設定で入力するURLは、https://Herokuアプリ名.herokuapp.com/qiita/webhooks
となるはずです。
module.exports = robot => { robot.router.post('/qiita/webhooks', (req, res) => { console.log(req.body); res.end(); }); }
URLの設定後、例として以下のような投稿をQiita:Teamで作成してみます。
その結果、Qiita:TeamからWebhookのPOSTリクエストが送信され、以下のようなリクエストボディを受け取ることができます。
{ action: 'created', item: { body: '<p>test</p>\n', coediting: false, comment_count: 0, created_at_as_seconds: 1492484003, created_at_in_words: '1分未満', created_at: '2017-04-18 11:53:23 +0900', id: 12345, lgtm_count: 0, raw_body: 'test\n', stock_count: 0, stock_users: [], tags: [], title: 'test', updated_at: '2017-04-18 11:53:23 +0900', updated_at_in_words: '1分未満', url: 'https://vasily.qiita.com/Horie1024/items/1234567890abcdefghijk', user: { id: 12345, profile_image_url: 'https://qiita-image-store.s3.amazonaws.com/profile-images/xxxxxx', url_name: 'Horie1024' }, uuid: '1234567890abcdefghijk' }, model: 'item', user:{ id: 12345, profile_image_url: 'https://qiita-image-store.s3.amazonaws.com/profile-images/xxxxxx', url_name: 'Horie1024' } }
リクエストボディのaction
プロパティがcreated
、model
がitem
であることから記事の新規作成であることがわかります。
また、item
プロパティはItem型で表され、ドキュメントのTypes#ItemでItem型の各プロパティがどのような意味を持つのか確認できます。例えば、raw_body
プロパティは記事本文、body
プロパティは記事本文のHTML表現を表します。
.textlintrcの設定
検証ルールは、書籍執筆用のルールをベースにVASILY DEVELOPERS BLOGの体裁に合わせてカスタマイズしています。簡単な表記ゆれの対策には、textlint-rule-prhを利用しています。
{ "rules": { "prh": { rulePaths: ["prh.yml"] }, "max-ten":{ max: 3 }, "spellcheck-tech-word": true, "no-mix-dearu-desumasu": true, "no-exclamation-question-mark": false, "preset-ja-technical-writing": { "no-exclamation-question-mark": { "allowHalfWidthExclamation": false, "allowHalfWidthQuestion": false, "allowFullWidthExclamation": true, "allowFullWidthQuestion": true } }, "preset-jtf-style": true } }
以下はprhの定義ファイルです。
version: 1 rules: - expected: IQON pattern: - iqon - iQON - expected: VASILY pattern: - vasily - Vasily
Webhookからの記事本文の取得とtextlintの実行
Webhookからの記事の投稿・更新を受け、textlintでの文章校正を実行します。TextLintEngineCore#executeOnText
を使用すると引数に取ったテキストに対してtextlintを実行することができます。第二引数にはMarkdown形式のファイルである事を示すために.md
を指定します。そして、校正結果についてTextLintEngineCore#isErrorResults
で確認し、エラーが有ればエラー内容をQiitaのコメントで確認しやすいようフォーマットします。
robot.router.post("/qiita/webhooks", (req, res) => { let item = req.body.item; // Webhookで受け取った記事本文をexecuteOnTextに渡す // 第二引数には".md"を指定 engine.executeOnText(item.raw_body, ".md").then(results => { if (engine.isErrorResults(results)) { // エラー有り // 結果を確認しやすいようフォーマット } else { // エラー無し } }); });
文章校正の結果をフィードバック
文章校正の結果は、記事へのコメントとしてフィードバックします。コメントの投稿・編集にはQiita APIを通じて行うため、簡単なAPIクライアントを作成しています。
以下のコードでは、校正結果をコメントとして投稿しています。
const Qiita = require('../libs/Qiita'); const qiita = new Qiita({team: "vasily", token: process.env.YOUR_QIITA_TOKEN}); robot.router.post("/qiita/webhooks", (req, res) => { let item = req.body.item; // Webhookで受け取った記事本文をexecuteOnTextに渡す // 第二引数には".md"を指定 engine.executeOnText(item.raw_body, ".md").then(results => { if (engine.isErrorResults(results)) { // エラー有り let output = ... // 結果を確認しやすいようフォーマット qiita.Comments.post(item.uuid, output); } else { // エラー無し qiita.Comments.post(item.uuid, "エラーはありません"); } }); });
また、既に文章校正の結果が投稿されている場合、コメントを上書きするようにしています。
ここまでのコードの詳細はこちらを御覧ください。
https://github.com/horie1024/hubot-textlint-sample/blob/master/scripts/hubot-textlint.js
実行結果
Qiita:Teamにtechblog
タグを付けて投稿・編集すると以下のように校正結果がコメントとして投稿されます。自分で自分にフィードバックしている形になっていますが、本来ならbot用にサービスアカウントを用意したいところです。Qiita:Teamでサービスアカウントが利用できないため、暫定で自分で発行したアクセストークンを使用しています。
まとめ
Qiita:Teamへの投稿をトリガーに文章校正を実行することができました。textlintは非常に素晴らしいツールで、既存の他のツールと連携させることも簡単に行うことができます。校正ルールはまだ見直しの余地がありますが、今回の事を切っ掛けにみんながより文章を書きやすい環境を作っていけたらと思います。
最後に
VASILYでは、現在新規サービスの開発を行っています。少しでもご興味のある方のご応募をお待ちしています。