FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

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

プログラマの三大美徳

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

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

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

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

例えば、

  • プルリクのレビュワーをセットする
    f:id:taketor:20150722134833p:plain

  • グループチャットツールで送り先を指定する
    f:id:taketor:20150722134906p:plain

チームが3、4人程度ならまだ苦でもないのですが10人ぐらいのチームになると、

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

(# ゚Д゚)メンドクセェェ!!

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

作るもの

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に追加すれば完成ですね。
f:id:taketor:20150722135006p:plain

ブックマーククリックすることで
f:id:taketor:20150722135034p:plain

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);


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

f:id:taketor:20150722135109p:plain

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

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

f:id:taketor:20150722135131p:plain

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に関しては証明書をオレオレ証明書の作成を参考に作り、myCAディレクトリ以下に設置しました。

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でポートと証明書パスの設定

config.yml

port: 8443
cert_path:
  certificate: ./myCA/server.crt
  private_key: ./myCA/server.key


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

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}


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

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なら月1万円ぐらいまで自由に使えるので、そこに設置してチームで運用してみたいと思います。

コードはGithubに置いておくので興味のある方はご自由にお使いください。

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

ブックマークレット作成時に参考にさせていただいサイトソーシャルてんこ盛り - actyway -

この記事はQiitaに投稿したものの転載です。