Chrome拡張を作ってweb版ChatWorkを改造してみた

こんにちは。
とても今更ながら、Chrome拡張機能がHTMLとCSSとjsで作れるらしいので、作ってみました。

よくあるサンプルの、「ただalert出すだけ」だと芸がないので、
web版のChatWorkを改造しつつ、拡張機能について学んだメモです。

注意

この記事を執筆したのは2014/07/20です。

ChatWorkのHTML構造に激しく依存するため、
HTMLやjsの回収があった場合には動作しなくなる可能性が非常に高いです。
情報が古くなり動かなくなる可能性があることを、あらかじめご了承ください。

制作するもの

コナミコマンドを実装するjsを利用して、

ChatWorkのページ内で、
コナミコマンド(↑↑↓↓←→←→ba)が入力されたら、マイチャットにグラディウスのAAを投稿する

というしょうもない小ネタをやってみたいと思います。
ちなみに完成した物はこちらになります。

前提

ライブラリの導入にbowerを使用します。
インストールされている前提で話を進めます。ご了承下さい。

まずはalert

とりあえず動作確認のために、
ChatWorkのドメイン(https://www.chatwork.com)ならalertを出すようにしてみます。

cd /path/to/work
mkdir chrome-ext-gradius && cd chrome-ext-gradius
bower init # 適当に入力()
bower install --save cheet.js jquery
touch manifest.json main.js chatwork-util.js
echo "bower_components" >> .gitignore
git init
git add .
git commit -m "initial commit"

コナミコマンドを実現するjs、jQueryはbowerで配布されています。
あとは拡張機能に必要なmanifest.jsonと、自作するjsファイルを生成します。

manifest.jsonは以下の内容にします。

{
    "version": "1.0.0",
    "manifest_version": 2,

    "name": "CONAMI command for ChatWork",
    "description": "コナミコマンドを入力するとChatWorkに何かが起こる・・・!",

    "content_scripts": [
        {
            "matches": ["https://www.chatwork.com/*"],
            "js": [
                "bower_components/jquery/dist/jquery.min.js",
                "bower_components/cheet.js/cheet.min.js",
                "chatwork-util.js",
                "main.js"
            ]
        }
    ]
}

そして、main.jsにアラートだけ書きます。

alert('ChatWorkです');

この状態で、Chrome拡張機能としてChromeにインポートして動作確認します。

拡張機能をChromeにインポート

ストアで販売してない拡張機能でもインポートできます。

メニューから拡張機能のページを開き、

Step01

「パッケージ化されていない拡張機能を読み込む…」ボタンをクリックします。
選択するファイルは、先ほど作成したchrome-ext-gradiusフォルダです。

フォルダを選択すると、下記の画面のように拡張機能の一覧に現れます。

Step02

なお、一度取り込みを行ってしまえば、
ファイルを変更した時に拡張機能のページをリロードするだけで再読込されます。

リロードしないと拡張機能の再読み込みがされないので、
あれ動かない?と思ったら拡張機能のページをリロードしてみてください。

次に、Chrome拡張機能のcontent scriptsについての理解を深めます。

content scripts

content scriptsは任意のwebページに対して任意のjsを実行できますが、いくつか制限があります。

コンテント・スクリプトは "Isolated World"(隔離空間) と呼ばれる特殊な環境で実行される。ここでは読み込まれたページのDOMにはアクセスできるが、該当ページのJavaScriptの変数や関数には出来ない。そのため、各コンテンツ・スクリプトからは、あたかも他のJavaScriptが全く実行されていないページで自分が実行されるように見える。逆もまた真なりで、各ページのスクリプトからもコンテンツ・スクリプトで定義した変数や関数にアクセスすることは出来ない。
コンテント・スクリプト | Chrome Extensions API リファレンス

このように、そのページで読み込まれているjsには一切アクセスできず、
拡張機能で定義した機能についてもそのページのjsからはアクセスできません。

ただし、DOMを介してデータのやりとりを行うことができます。

「DOMを書き換えることができて、どんな命名をしても対象ページ内のjsと衝突することはない」

と捉えるとかなり幸せに思いますが、
特定のページ用(今回ならChatWork専用)の拡張機能を作る場合には、
ChatWorkで動作しているjsにアクセスできないため、いちいち各操作を自前で実装する必要があります。
例えば、チャットルームの切り替えは、タイミング制御まで合わせるとなかなかの鬼門です。

不幸中の幸いながら、
ChatWorkのHTMLには属性として各種パラメータが埋め込まれているので、パース処理や値の取得自体は簡単です。

では早速実装していきます。

コナミコマンドを有効化

基本的にはcheet関数を使用してショートカットを設定するだけなのですが、

キーボードイベントに関して要注意なのは、ChatWorkのjsと衝突して、
↑キーと↓キーが奪われてしまい、しかも握りつぶされています。

cheet.jsはwindowにイベントを設定しているので、
documentで止められてしまうと反応できません。

つらい。これはつらい。

と思って心が折れかけましたが、無理やりイベントを先取りすることが出来ました。

// 握りつぶされる前にkeydownイベントをwindowオブジェクトに伝播させる
$(document).on('keydown', function(e) {
    var ev = new Event('keydown');
    ev.keyCode = e.keyCode;

    window.dispatchEvent(ev);
});

cheet('U U D D L R L R b a', function() {
    alert('コナミコマンド発動!');
});

カスタムイベントを作りwindowへぶん投げるようにしました。

拡張機能をリロードし、ChatWorkもリロードし、
「↑ ↑ ↓ ↓ ← → ← → B A」と入力してみると、以下のようになります。

Step03

cheet.js側が反応するようになったのでOKです。次へ進みます。

ユーティリティを作成

  • チャットルームを切り替える
  • チャットに投稿する
  • ChatWork記法を使う

などの部分を直接書くと煩雑になるため、
DOM操作や記法のユーティリティは、作成しておいたchatwork-util.jsのほうに記述していきます。

[info]タグ記法のテキストを生成するユーティリティ

ChatWorkでは、[info]というタグが使えます。
AAなど等幅にしてほしいテキストを送る場合に便利です。

使用するのでユーティリティ化します。

(function(global) {
    'use strict';

    var util = {
        /**
         * 指定されたテキストをChatWork記法の[info]タグへ変換する
         * @param string body    [info]タグの本文にあたる部分
         * @param string [title] 指定されたら[title]タグも使用する。省略された場合使用しない
         * @return string [info]...[/info]で囲われた文字列
         */
        toInfomation: function(body, title) {
            var info = '[info]';

            // titleが指定されていたらtitleタグを使用
            if(typeof title !== 'undefined')
                info += '[title]' + title + '[/title]';

            info += body + '[/info]';
            return info;
        },
    };

    global.util = util;
}(this));

マイチャットのHTML要素を取得するユーティリティ

ChatWorkの左側にあるチャットルーム一覧は、
頻繁にDOMの書き換えが行われるため、変数にキャッシュすると期待通りの動作になりません。
なのでメソッド化して、毎度HTMLに問い合わせて取得します。

なお、全体を記述すると長くなってしまうので、
ファイル全体ではなく必要な箇所のみ記述します。

var util = {
    /**
     * マイチャットの名前。もし名前を変更している人がいたらここを書き換えてください。
     */
    MYCHAT_NAME: 'マイチャット',

    /**
     * マイチャットへのリンクを表す要素を取得する
     * NOTE: チャット一覧のDOMは頻繁に書き換わるため変数にキャッシュすると期待道理に動かない。
     *       なので変数にキャッシュせずに毎回取得を行う
     * NOTE: MYCHAT_NAMEに設定されている名前のチャットをマイチャットとみなす
     * @return jQuery マイチャットを表すjQueryオブジェクト
     */
    getMyChat: function() {
        // チャットルーム名がMYCHAT_NAMEのチャットを返す
        var selector = 'li[aria-label="' + this.MYCHAT_NAME + '"]';
        return $('#_roomListItems').find(selector);
    }

    // ...
};

マイチャットを取得出来ました。
何らかのフラグではなく、チャットルームの名前とMYCHAT_NAMEという変数の値に依存してしまっているので、治せる方法があれば治したいです。

マイチャットへ切り替えるユーティリティ

マイチャットの要素を取得できたので、マイチャットへ切り替えるユーティリティも作成しておきます。

実装は簡単で、単にclickイベントを発行するだけです。

var util = {
    /**
     * 現在のチャットをマイチャットへ切り替える
     * @return void
     */
    changeMyChat: function() {
        this.getMyChat().trigger('click');
    }

    // ...
};

これだけです。

…なわけありません。
チャットルームの切替時に非同期でAjaxが走り、
切り替え先のチャットのメッセージ一覧を取得しメッセージ一覧画面をかきえます。
このチャットルームの切り替え完了を検知するタイミングの制御が、なかなか難しいです。

制御に失敗すると、チャットへ投稿するときに、別のチャットへ投稿してしまうことが起こります。
ですが、詳細までお話していると今回本筋から大きくそれるため触れません。
MutationObserverというオブジェクトを利用しています。詳しくはリポジトリを御覧ください。

チャットに投稿するユーティリティ

前述のタイミング制御の問題さえなければごくシンプルです。

var util = {
    /**
     * 現在のチャットへ発言する
     * @param string body 発言内容
     * @return void
     */
    send: function(body) {
        util.$chatText.val(body);
        util.$sendBtn.trigger('click');
    }
};

$(function() {
    // チャット内容入力フォーム、送信ボタンを変数にキャッシュ
    util.$chatText = $("#_chatText");
    util.$sendBtn = $("#_sendButton");
});

チャット内容入力フォームと送信ボタンのHTMLは、
書き換えられることがないので、速度のため変数にキャッシュしています。

動作確認をしてみます。

$(function() {
    util.send('hogehoge');
});

Scree

OKです。

さて、マイチャットに切り替えられるようになり、発言もできるようになったので、
早速AAを投稿してみたいと思います。

AAを投稿する

今回使用するAAはこちら。

         _ _
        (_ _ )―――――――――――
   ぴちゅーん
                   _ _
                  (_ _ )―――――――――――
   _
  _ヽ ヽ.__________ 
≡] _三 ,. ヽ_ 丶-`゙__=- ―――――――――――
    ∥_,"=== ̄ ̄
                  ぴちゅーん
                           _ _
                          (_ _ )―――――――――――
              ぴちゅーん
         _ _
        (_ _ )――――――――

はい。ビックバイパーです。グラディウスやったことないけど。

ナメてんのかって感じの良いAAですね。
こちらのサイト様に掲載されていたAAを少し改変させていただきました。

あまり複雑なAAだと分かりにくいと思ったので、シンプルなAAで行きます。

ユーティリティは揃えらので、
あとはもうコナミコマンドのコールバックを仕上げるだけです。

main.jsの内容は以下の通りになります。

(function(global) {
    'use strict';

    var AA = util.toInfomation("         _ _\n\
        (_ _ )―――――――――――\n\
   ぴちゅーん\n\
                   _ _\n\
                  (_ _ )―――――――――――\n\
   _\n\
  _ヽ ヽ.__________ \n\
≡] _三 ,. ヽ_ 丶-`゙__=- ―――――――――――\n\
    ∥_,\"=== ̄ ̄\n\
                  ぴちゅーん\n\
                           _ _\n\
                          (_ _ )―――――――――――\n\
              ぴちゅーん\n\
         _ _\n\
        (_ _ )―――――――――――", 'コナミコマンドが入力されました')\;\

    // 握りつぶされる前にkeydownイベントをwindowオブジェクトに伝播させる
    $(document).on('keydown', function(e) {
        var ev = new Event('keydown');
        ev.keyCode = e.keyCode;

        window.dispatchEvent(ev);
    });

    // コナミコマンドが入力されたらマイチャットへ切り替えて発言を行う
    cheet('U U D D L R L R b a', function() {
        util.changeMyChat(function() {
            // 切り替わったらAAを投稿
            util.send(AA);
        });
    });
}(this));

コナミコマンドを入力してみます。

Scree

OKです。

どのチャットに居てもマイチャットへ移動し、AAが投稿されます。
動作がとても重い環境だと投稿しそこねたり、マイチャット以外へ投稿してしまうこともあるのでご注意ください。

ということで拡張機能が完成しました。
拡張機能ではCSSを読み込んだりすることもできるようなので、
もっと深めてみたら面白いことになりそうです。

完成したファイルはこちらからご覧になることができます。

Leko/chrome-ext-gradius · GitHub

参考

Google Chrome拡張機能入門 (全20回) – プログラミングならドットインストール

コンテント・スクリプト | Chrome Extensions API リファレンス

グラディウスAA集 (ボスのアスキーアート)