プログラマの三大美徳に
- 怠慢(Laziness)
- 短気(Impatience)
- 傲慢(Hubris)
ってありますが、大して短気でも傲慢でもないので、とりあえずは怠慢になろうとしている新卒1年目の怠慢エンジニアです。
日々のちょっとしたストレス
チームで開発していると開発メンバーの名前を入力しないといけない場面て多くありませんか?
例えば、
- プルリクのレビュワーをセットする
- グループチャットツールで送り先を指定する
チームが3、4人程度ならまだ苦でもないのですが10人ぐらいのチームになると、
プルリクやレビューをお願いする度に、
名前を入力して送信相手に漏れがないか確認してという作業を、
一日に何度も何度も何度も、、、
(# ゚Д゚)メンドクセェェ!!
となってしまったので、チームメンバーをワンポチでレビュワーや送信相手に指定できるボタンをブックマークレットで作ってやろうと思い立ちました(^ω^#)
ブックマークレットとは、
ブックマーク (お気に入り) を利用してブラウザに便利な機能を追加するものです。(引用:はてなブックマークレット)
作るもの
To付きメッセージを送る時、固定メンバーを一括追加できるブックマークレット
※会社ではChatworkを使用しているのでコードはChatwork用です。
必要最小限の機能のみ
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に追加すれば完成ですね。
ブックマークをクリックすることで
hogeさんとfugaさんとfooさんを追加できましたね。
いい感じです!
これはこれでシンプルでいいのですが、
一つの固定メンバーしか追加できませんし、もう少し高機能にしたいなと、、
ちょっと便利に
さっきのブックマークレットを改良して、以下の機能を追加していきます。
- 開発メンバーかプロジェクトメンバー全員かを選べる
- 名前のありorなしを設定できる
- 名前の最後に改行を追加するかを設定できる
- デザインいい感じに
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);
登録したブックマークをクリックすることで、
ブラウザの右上に上記のようなツールセットが表示されます。
これでボタンをポチるだけで、
Engineer1, Engineer2, Engineer3をセットすることができました!
いい感じですね。
ただ、自分一人だけが使うだけならこれで問題ないのですが、せっかく作ったのでメンバーに配布したいなと、
でもメンバーが増えたり減ったりする度にブックマークレットを配布しなおして再度ブックマークに登録してもらうのは手間だなーと、、
メンテナンス性を高めるために
もっと惰性を追求したいので、ブックマークレットをサーバーから配信するかたちに変更します。
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ディレクトリ以下に設置しました。
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でポートと証明書パスの設定
port: 8443
cert_path:
certificate: ./myCA/server.crt
private_key: ./myCA/server.key
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}
以下がメインの配信サーバーです。
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 -