新人エンジニアが業務効率化のためにブックマークレット作ってみたよ


プログラマの三大美徳に

* 怠慢(Laziness)
* 短気(Impatience)
* 傲慢(Hubris)

ってありますが、大して短気でも傲慢でもないので、とりあえずは怠慢になろうとしている新卒１年目の怠慢エンジニアです。

## 日々のちょっとしたストレス

チームで開発していると**開発メンバーの名前を入力しないといけない場面**て多くありませんか？

例えば、

* **プルリクのレビュワーをセットする**

![20151228174228.png](https://qiita-image-store.s3.amazonaws.com/0/7885/191e9367-b276-750e-fb7a-95a48cc5ef00.png "20151228174228.png")

* **グループチャットツールで送り先を指定する**

![20151228174538.png](https://qiita-image-store.s3.amazonaws.com/0/7885/35ab8132-0cf5-e67e-fe55-877873c69394.png "20151228174538.png")

チームが３、４人程度ならまだ苦でもないのですが１０人ぐらいのチームになると、

プルリクやレビューをお願いする度に、
**名前を入力して送信相手に漏れがないか確認して**という作業を、
一日に何度も何度も何度も、、、

## (# ﾟДﾟ)ﾒﾝﾄﾞｸｾｪｪ!!

となってしまったので、チームメンバーをワンポチでレビュワーや送信相手に指定できるボタンをブックマークレットで作ってやろうと思い立ちました（＾ω＾#）

**ブックマークレットとは、**
> *ブックマーク (お気に入り) を利用してブラウザに便利な機能を追加するものです。*(引用：[はてなブックマークレット](http://www.hatena.ne.jp/tool/bookmarklet))

## 作るもの
#### **To付きメッセージを送る時、固定メンバーを一括追加できるブックマークレット**

※会社ではChatworkを使用しているのでコードはChatwork用です。

## 必要最小限の機能のみ

```javascript:bookmarklet
javascript: (function(d) {
    cw_textarea = d.getElementById('_chatText');
    cw_textarea.value = cw_textarea.value + '[To:0000]hoge [To:0000]fuga [To:0000]foo ';
    cw_textarea.focus();
})(document);
```
これをブックマークのURLに追加すれば完成ですね。

![スクリーンショット 2014-12-28 19.49.57.png](https://qiita-image-store.s3.amazonaws.com/0/7885/1089c872-fd34-7320-af61-9a01a2d04266.png "スクリーンショット 2014-12-28 19.49.57.png")

ブックマークをクリックすることで

![スクリーンショット 2014-12-28 20.17.39.png](https://qiita-image-store.s3.amazonaws.com/0/7885/8338f18a-de6a-bb33-ad6c-e0cb43a7be3f.png "スクリーンショット 2014-12-28 20.17.39.png")

hogeさんとfugaさんとfooさんを追加できましたね。
いい感じです！

これはこれでシンプルでいいのですが、
一つの固定メンバーしか追加できませんし、もう少し高機能にしたいなと、、


## ちょっと便利に

さっきのブックマークレットを改良して、以下の機能を追加していきます。

 * 開発メンバーかプロジェクトメンバー全員かを選べる
 * 名前のありorなしを設定できる
 * 名前の最後に改行を追加するかを設定できる
 * デザインいい感じに


```javascript:bookmarklet
javascript: (function(d) {
  var url = location.href,
    bookmarklet = d.createElement('div');
  bookmarklet.id = 'bookmarklet';
  bookmarklet.innerHTML += '<div class="buttons">' +
      '<div class="cw-wrapper">' +
        '<div class="cw-buttons clearfix">' +
          '<span class="buttons-name">' +
            '<img src="https://www.chatwork.com/image/favicon/favicon00.ico">Chatwork:' +
          '</span>' +
          '<button type="button" onclick="setChatworkMember(\'dev\')">エンジニアメンバーをセット</button>' +
          '<button type="button" onclick="setChatworkMember(\'all\')">すべてのメンバーをセット</button>' +
        '</div>' +
        '<div class="cw-checkboxes" clearfix>' +
          '<input id="delete_newline" type="checkbox" name="delete_newline" checked><label for="delete_newline">改行削除</label>' +
          '<input id="has_name" type="checkbox" name="has_name" checked><label for="has_name">名前あり</label>' +
      '</div>' +
    '</div>' +

    '<div onclick="document.body.removeChild(document.body.firstChild);" class="close">閉じる</div>' +

    '<style>' +
    '#bookmarklet {position: fixed; z-index: 10000; top: 10px; right: 10px; width: 340px; height: 105px; border: solid 6px #C5DCEA; border-radius: 5px; box-shadow: black 1px 1px 8px; font-size: 13px; font-family: sans-serif; color: #4f4f4f; text-align: left; padding: 0; margin: 0; background-color: rgb(223, 244, 255); } #bookmarklet .buttons {margin: 10px 0;padding: 10px; height: 68px; } #bookmarklet .buttons button {width: 200px; float: right; } #bookmarklet .cw-buttons, #bookmarklet .stash-buttons {margin: 5px 10px 5px 0px; } #bookmarklet .cw-checkboxes label {margin-right: 10px; } #bookmarklet .buttons-name {font-size: 15px; font-weight: bold; text-align: right; width: 100px; } #bookmarklet .buttons-name img {width:17px; margin: 0px 3px -1px; } #bookmarklet .close {height:20px; width:80px; background-color:#009fff; color:white; text-align:center; font-size:15px; top:-10px; left:240px; line-height:1; padding:4px 0 0 0; position:absolute; cursor:pointer; } #bookmarklet .clearfix:after {content: ""; clear: both; display: block; }' +
    '</style>';

  setChatworkMember = function(group) {
    if (location.hostname.indexOf('chatwork') == -1) {
      alert('チャットワークのページで実行してください');
      return;
    }
    var cw_textarea = d.getElementById('_chatText');
    cw_textarea.value = cw_textarea.value + getChatworkToMmmbers(group);
    cw_textarea.focus();
  };

  getChatworkToMmmbers = function(group) {
    var members = [
      {'id': 5000000, 'position': 'engineer', 'name': 'Engineer1'},
      {'id': 5000001, 'position': 'engineer', 'name': 'Engineer2'},
      {'id': 5000002, 'position': 'engineer', 'name': 'Engineer3'},
      {'id': 5000003, 'position': 'director', 'name': 'Director1'},
      {'id': 5000004, 'position': 'director', 'name': 'Director2'},
      {'id': 5000005, 'position': 'director', 'name': 'Director3'}
    ];
    var to_texts = {};
    for (var i in members) {
      var to_text = '[To:' + members[i].id + ']';
      if (d.getElementById('has_name').checked) {
        to_text += members[i].name;
      }
      to_text += (!d.getElementById('delete_newline').checked) ? '\n' : ' ';
      to_texts[members[i].id] = to_text;
    }
    var to_members = '';
    switch (group) {
      case 'dev':
        for (var i in members) {
          if (members[i].position === 'engineer') {
            to_members += to_texts[members[i].id];
          }
        }
        break;
      case 'all':
        for (var i in members) {
          to_members += to_texts[members[i].id];
        }
        break;
    }
    return to_members;
  };
  d.body.insertBefore(bookmarklet, document.body.firstChild);
})(document);
```

登録したブックマークをクリックすることで、

![スクリーンショット 2014-12-28 21.12.26.png](https://qiita-image-store.s3.amazonaws.com/0/7885/c5ef7822-b5bb-701b-c652-0d08fc48ce8c.png "スクリーンショット 2014-12-28 21.12.26.png")

ブラウザの右上に上記のようなツールセットが表示されます。

これでボタンをポチるだけで、

![スクリーンショット 2014-12-28 21.20.37.png](https://qiita-image-store.s3.amazonaws.com/0/7885/2c903723-3d1e-997a-53a9-4754ba79fb27.png "スクリーンショット 2014-12-28 21.20.37.png")

Engineer1, Engineer2, Engineer3をセットすることができました！

いい感じですね。


ただ、自分一人だけが使うだけならこれで問題ないのですが、せっかく作ったのでメンバーに配布したいなと、

でもメンバーが増えたり減ったりする度にブックマークレットを配布しなおして再度ブックマークに登録してもらうのは手間だなーと、、


## メンテナンス性を高めるために
もっと惰性を追求したいので、**ブックマークレットをサーバーから配信する**かたちに変更します。

```javascript:bookmarklet
javascript: (function(d) {
    var s = d.createElement('script');
    s.id = 'bookmarklet';
    s.charset = 'UTF-8';
    s.src = 'https://[配信サーバーのホスト]:8443/bookmarklet?time=' + (new Date()).getTime();
    d.body.appendChild(s)
})(document);
```

ブックマークレットの方はだいぶスッキリしましたね。
続いて、JavaScriptを配信するサーバーサイドです。

#### サーバサイドで使ったもの
 * 言語：Ruby(2.1.3p242)
 * サーバー:Webrick
 * フレームワーク：Sinatra

SSLを使っているサービス上からは外部のJavaScriptを勝手に読み込むことはできなかったので配信サーバーでもSSLを使用しています。

ちなみに、読み込もうとするとSSL通信じゃないとダメだおってエラーが吐かれます。
> Mixed Content: The page at 'https://www.chatwork.com/' was loaded over HTTPS, but requested an insecure script 'http://localhost:8443/bookmarklet'. This request has been blocked; the content must be served over HTTPS.

SSLに関しては証明書を[オレオレ証明書の作成](http://qiita.com/kawa123/items/81a71cb55db87cdbba4d)を参考に作り、myCAディレクトリ以下に設置しました。
<br>

```Ruby:Gemfile
source "https://rubygems.org"

gem "rack"
gem "webrick"
gem "openssl"
gem "net"
gem "sinatra"
gem "sinatra-contrib"
gem "sinatra-cross_origin", "~> 0.3.1"
gem "json"
```

config.ymlでポートと証明書パスの設定

```ruby:config.yml
port: 8443
cert_path:
  certificate: ./myCA/server.crt
  private_key: ./myCA/server.key
```

members.ymlでメンバーの情報を管理

```ruby:members.yml
- {id: 5000000, position: engineer, name: Engineer1}
- {id: 5000001, position: engineer, name: Engineer2}
- {id: 5000002, position: engineer, name: Engineer3}
- {id: 5000003, position: director, name: Director1}
- {id: 5000004, position: director, name: Director2}
- {id: 5000005, position: director, name: Director3}
```

以下がメインの配信サーバーです。

```ruby:bookmarklet_provider.rb
require 'webrick'
require 'webrick/https'
require 'openssl'
require 'sinatra'
require 'sinatra/base'
require 'sinatra/cross_origin'
require 'sinatra/reloader'
require 'net/https'
require 'json'
require 'yaml'

set :environment, :production

conf = YAML.load_file('config.yml')
CURRENT_DIR_PATH = Dir.pwd

class BookmarkletProvider < Sinatra::Base
    register Sinatra::CrossOrigin

    before do
        cross_origin
        content_type :js
    end

    get '/bookmarklet' do
        members = YAML.load_file(File.join(CURRENT_DIR_PATH, 'members.yml'))

        <<-EOS
        (function(d) {
          var url = location.href,
            bookmarklet = d.createElement('div');
          bookmarklet.id = 'bookmarklet';
          bookmarklet.innerHTML += '<div class="buttons">' +
            '<div class="cw-wrapper">' +
            '<div class="cw-buttons clearfix">' +
            '<span class="buttons-name">' +
            '<img src="https://www.chatwork.com/image/favicon/favicon00.ico">Chatwork:' +
            '</span>' +
            '<button type="button" onclick="setChatworkMember(\\'dev\\')">エンジニアメンバーをセット</button>' +
            '<button type="button" onclick="setChatworkMember(\\'all\\')">すべてのメンバーをセット</button>' +
            '</div>' +
            '<div class="cw-checkboxes" clearfix>' +
            '<input id="delete_newline" type="checkbox" name="delete_newline" checked><label for="delete_newline">改行削除</label>' +
            '<input id="has_name" type="checkbox" name="has_name" checked><label for="has_name">名前あり</label>' +
            '</div>' +
            '</div>' +

            '<div onclick="document.body.removeChild(document.body.firstChild);" class="close">閉じる</div>' +

            '<style>' +
            '#bookmarklet {position: fixed; z-index: 10000; top: 10px; right: 10px; width: 340px; height: 105px; border: solid 6px #C5DCEA; border-radius: 5px; box-shadow: black 1px 1px 8px; font-size: 13px; font-family: sans-serif; color: #4f4f4f; text-align: left; padding: 0; margin: 0; background-color: rgb(223, 244, 255); } #bookmarklet .buttons {margin: 10px 0;padding: 10px; height: 68px; } #bookmarklet .buttons button {width: 200px; float: right; } #bookmarklet .cw-buttons, #bookmarklet .stash-buttons {margin: 5px 10px 5px 0px; } #bookmarklet .cw-checkboxes label {margin-right: 10px; } #bookmarklet .buttons-name {font-size: 15px; font-weight: bold; text-align: right; width: 100px; } #bookmarklet .buttons-name img {width:17px; margin: 0px 3px -1px; } #bookmarklet .close {height:20px; width:80px; background-color:#009fff; color:white; text-align:center; font-size:15px; top:-10px; left:240px; line-height:1; padding:4px 0 0 0; position:absolute; cursor:pointer; } #bookmarklet .clearfix:after {content: ""; clear: both; display: block; }' +
            '</style>';

          setChatworkMember = function(group) {
            if (location.hostname.indexOf('chatwork') == -1) {
              alert('チャットワークのページで実行してください');
              return;
            }
            var cw_textarea = d.getElementById('_chatText');
            cw_textarea.value = cw_textarea.value + getChatworkToMmmbers(group);
            cw_textarea.focus();
          };

          getChatworkToMmmbers = function(group) {
            var members = #{members.to_json};
            var to_texts = {};
            for (var i in members) {
              var to_text = '[To:' + members[i].id + ']';
              if (d.getElementById('has_name').checked) {
                to_text += members[i].name;
              }
              to_text += (!d.getElementById('delete_newline').checked) ? '\\n' : ' ';
              to_texts[members[i].id] = to_text;
            }
            var to_members = '';
            switch (group) {
              case 'dev':
                for (var i in members) {
                  if (members[i].position === 'engineer') {
                    to_members += to_texts[members[i].id];
                  }
                }
                break;
              case 'all':
                for (var i in members) {
                  to_members += to_texts[members[i].id];
                }
                break;
            }
            return to_members;
          };
          d.body.insertBefore(bookmarklet, document.body.firstChild);
        })(document);

        EOS
    end

    get '/permission_for_ssl' do
        'You can SSL connection with your browser.'
    end

end

webrick_options = {
    :Port            => conf['port'],
    :ServerType      => WEBrick::Daemon,
    :SSLEnable       => true,
    :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE,
    :SSLCertificate  => OpenSSL::X509::Certificate.new(  File.open(conf['cert_path']['certificate']).read),
    :SSLPrivateKey   => OpenSSL::PKey::RSA.new(          File.open(conf['cert_path']['private_key']).read),
    :SSLCertName     => [ [ 'CN',WEBrick::Utils::getservername ] ],
}
Rack::Handler::WEBrick.run BookmarkletProvider, webrick_options
```

クロスドメイン対策のためにCrossOriginをbeforeで設定しています。

/bookmarkletには、先ほどのブックマークレットを直書きしてmembersをyamlから読み込むようにした感じですね。

またオレオレ証明書なので、ブラウザから最初にこのサーバーにアクセスするとき警告が出てしまいます。

具体的には、以下みたいな感じです。

*Chromeの場合*
> GET https://localhost:8443/bookmarklet?time=1419733313579 net::ERR_INSECURE_RESPONSE

*Safariの場合*
> Failed to load resource: このサーバの証明書は無効です。“localhost”に偽装したサーバに接続している可能性があり、機密情報が漏えいするおそれがあります。

そのため、**初回のみブラウザからアクセス許可を出す必要があり**、そのときように/permission_for_sslを追加しました。

webrick_optionsではポート番号とSSL、それとデーモン起動の設定をしています。

これで完成です！

```
$ ruby bookmarklet_provider.rb
```
を実行すればサーバーが起動できるので、あとはブックマークレットをポチるだけですね！！

## まとめ的なやつ

これで怠慢エンジニアにまた一歩近づくことができました！

ちょっとこだわりすぎてサーバーまで設置してしまいましたが、、

会社のAWSなら月１万円ぐらいまで自由に使えるので、そこに設置してチームで運用してみたいと思います。

コードは[Github](https://github.com/yoppe/bookmarklet)に置いておくので興味のある方はご自由にお使いください。

また、拙いコードですので改良点などのご指摘は大歓迎です。

ブックマークレット作成時に参考にさせていただいサイト[ソーシャルてんこ盛り - actyway -](http://actyway.com/t/%E3%82%BD%E3%83%BC%E3%82%B7%E3%83%A3%E3%83%AB%E3%81%A6%E3%82%93%E3%81%93%E7%9B%9B%E3%82%8A)