2016年を振り返る
すでに2017年が来ましたが振り返ります.
記事にしたやつ
Nearbyについて
ログを見ると1/2に書いているらしいのでよほど暇だったのでしょう.これについては過去記事にも書きましたね.
詳細についてそのうち書くみたいなことかいてますが,元気がなくかかれずに一年が経ってしまいました.おそらくコードも古いので一度書き直したほうがいいかもですね.
golangについて
2016年はgolangを頑張った気がします.頑張ったというか手を出したというか.入社後使うことはあるのかしら.
アクセスしてきたクライアントのグローバル側のIPとホストを出すサーバ github.com
Websocketでチャットをできるようにしたサーバ github.com
slackでおみくじできるようにしたサーバ github.com
画像のアップロードとそのホスティングをするサーバ github.com
golang結構書きやすくてよかった.一年に1つ新しい言語を学ぶ姿勢みたいなのがいいと聞いて始めたんだけど長い付き合いになりそう.
line botについて
LINE Botがdeveloper previewとして出た次の日ぐらいに書いたbotです.line echoでlinechoという名前になっています. 次の日ぐらいに書いたんですけどやはり最初は取れなかったですね.取ったところで同しようもないのですが. ただ,めちゃくちゃバズってた同じようなLINE echo botの記事で,ここが引っかかるよ!みたいな説明をみて同じのに引っかかった記憶があり,自らの手での体験は重要であるなぁと思った.
それ以外
研究
Android JavaとJavaScriptを書いてます.たまのツールでGolang
お仕事
Rubyと,JavaScript書きました.
まとめと雑感
研究でJava書きまくるのはいつもどおりでした.2016年の試みとしては,Golang,JavaScript(ReactとかES6とか)が新しいところになるのかな.2017年もこの調子で新しい言語をゆっくり覚えていきたい.
2017年は
Elixir勉強しています.ガンバルゾー
golangでparseのinstall数を集計するスクリプト書いた
書いた.
Parse.com使ってて(オンプレ移行は成功しそう),Parseに突っ込んでるログからインストール数の集計をできるようなコマンドを書いてみた.リポジトリに上げたいところなんだけどまだ,キーがハードコードされてるので治ったら.
要件
package main
package main import ( "fmt" "time" "bitbucket.org/xxx/xxxx/parseclient" // 見せられないよ ) var ( defaultApplicationID = "applicationID" defaultRestAPIKey = "restAPIKey" ) func printMonthlyCount(date string, count int) { fmt.Println(date, ": ", count) } func main() { client := parseclient.NewClient(defaultApplicationID, defaultRestAPIKey) androidCount, iosCount, dmAndroid, dmIos, err := client.FetchCount() if err != nil { panic(err) } fmt.Println(time.Now(), "のインストール数") fmt.Println("Android:", androidCount) fmt.Println("iOS:", iosCount) fmt.Println("月ごとの集計結果") fmt.Println("Android") dmAndroid.Each(printMonthlyCount) fmt.Println("iOS") dmIos.Each(printMonthlyCount) fmt.Println("合計:", androidCount+iosCount) }
取ってきて出力.dmAndroid.Each
とdmIos.Each
については後述
package dmap
空のときにデフォルト値が設定できるようなmapを定義
さっきのEach
はここ
http://ashitani.jp/golangtips/tips_map.html#map_Default
package dmap // Dmap Dictionaly map type Dmap struct { m map[string]int } // Get 値をGet func (d Dmap) Get(key string) int { v, ok := d.m[key] if ok { return v } return 0 } // Set 値をset func (d Dmap) Set(key string, value int) { d.m[key] = value } // Each ループ回せる func (d Dmap) Each(f func(string, int)) { for key, value := range d.m { f(key, value) } } // New func func New() *Dmap { return &Dmap{map[string]int{}} }
parseからの取得
package parseclient import ( "net/url" "strconv" "time" "bitbucket.org/xxx/xxxx/dmap" "github.com/facebookgo/parse" ) type device struct { CreatedAt time.Time `json:"createdAt"` Os string `json:"os"` } type result struct { Results []device `json:"results"` } // Client インストール数問い合わせのクライアント type Client struct { parseClient parse.Client } // NewClient Client生成 func NewClient(applicationID string, restAPIKey string) *Client { return &Client{ parseClient: parse.Client{ Credentials: parse.RestAPIKey{ ApplicationID: applicationID, RestAPIKey: restAPIKey, }, }, } } // FetchCount AndroidとiOSそれぞれカウントする func (c Client) FetchCount() (int, int, *dmap.Dmap, *dmap.Dmap, error) { jst, err := time.LoadLocation("Asia/Tokyo") if err != nil { return 0, 0, nil, nil, err } count := 0 androidCount := 0 iosCount := 0 dmAndroid := dmap.New() dmIos := dmap.New() for { var res result v := make(url.Values) v.Set("skip", strconv.Itoa(count)) v.Set("limit", "100") _, err := c.parseClient.Get(&url.URL{Path: "/1/classes/install_log", RawQuery: v.Encode()}, &res) if err != nil { return 0, 0, nil, nil, err } for _, element := range res.Results { jstTime := element.CreatedAt.In(jst) key := jstTime.Format("2006-01") if element.Os == "Android" { androidCount++ dmAndroid.Set(key, dmAndroid.Get(key)+1) } if element.Os == "iPhone OS" { iosCount++ dmIos.Set(key, dmIos.Get(key)+1) } } size := len(res.Results) count += size if size != 100 { break } time.Sleep(1000) // マナー } return androidCount, iosCount, dmAndroid, dmIos, nil }
実装は至ってシンプルで,APIをskipを使ってページ制御しながら,全件みつつOS種類に応じてカウントする. 月ごとの集計がだるかったので,dmapのkeyとして月を利用することでなんとかしました.ただ問題があって月ごとの並び順がバラバラになりがちなのでなんとかしたい.
感想
centos7にnsd構築してみた
nsd
DNSの権威サーバ実装.BINDが脆弱性祭りになりがちで大変なので試しに移行してみた記録.ところどころ抜けてるかもは思い出したりしたら追記する.
前提
設定するドメインは hoge.example.com
IPは 1.2.3.4
とする
インストール
ざっと調べた感じCentOS7ではnsdのパッケージがないっぽい?ので自力でビルドしていい感じに動かすこととする.
まず,https://www.nlnetlabs.nl/projects/nsd/ ここからnsdのソースを落としてきてビルドする.
# cd /usr/local/src # wget "https://www.nlnetlabs.nl/downloads/nsd/nsd-4.1.13.tar.gz" # tar zxvf nsd-4.1.13.tar.gz # cd nsd-4.1.13 # ./configure --prefix=/usr/local # make -j4 # make install
ところが今調べるとyumにあった.少し前までなかった気がする.とりあえず今回はソースからビルド
設定ファイルを書く
remote-control: control-enable: yes server: ip-address: "1.2.3.4" ip4-only: yes hide-version: yes database: "/etc/nsd/var/db/nsd/nsd.db" logfile: "/var/log/nsd.log" pidfile: "/etc/nsd/var/run/nsd.pid" chroot: "/etc/nsd" difffile: "/etc/nsd/var/db/nsd/ixfr.db" xfrdfile: "/etc/nsd/var/db/nsd/xfrd.state" zonelistfile: "/etc/nsd/var/db/nsd/zone.list" xfrdir: "/etc/nsd/tmp/" zonesdir: "/etc/nsd/zones" zone: name: "hoge.example.com" zonefile: "hoge.example.com.zone"
zonesdir
で指定したディレクトリにゾーン情報を書いていく
ゾーン情報
@ IN SOA ns.hoge.example.com. root.hoge.example.com. ( 2016113001 ; Serial 有効な開始日 3600 ; Refresh 900 ; Retry 3600000 ; Expire 3600 ) ; Minimum IN NS ns.hoge.example.com. @ IN A 1.2.3.4 fuga IN A 2.3.4.5 // ここにレコードを突っ込んでいく NS IN A 1.2.3.4
nsd-controlによる制御
まず,nsd-control-setup
をしてSSLキーを生成する.
できたら,
# nsd-control start
で起動できるはず.
どうも起動できてなければ,nsdを直接起動してみてエラーを見ると良い.
今後
元気が出たらDNSキャッシュサーバ実装のunboundも試す.
参考
RedmineのREST APIを拡張する話
RedmineのREST APIは良く出来てるんだけど,このカラムがほしいみたいなときにはどうすればよいのか.
結構かんたんな話で,api viewを作ればいい.参考:
この通りに拡張したい*.api.rsbを持ってきて,追記すれば良い.
たとえば今回issueの拡張がしたかったので,app/views/issues/index.api.rsb
を持ってきて
api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :limit => @limit) do @issues.each do |issue| api.issue do api.id issue.id api.project(:id => issue.project_id, :name => issue.project.name) unless issue.project.nil? api.tracker(:id => issue.tracker_id, :name => issue.tracker.name) unless issue.tracker.nil? api.status(:id => issue.status_id, :name => issue.status.name) unless issue.status.nil? api.priority(:id => issue.priority_id, :name => issue.priority.name) unless issue.priority.nil? api.author(:id => issue.author_id, :name => issue.author.name) unless issue.author.nil? api.assigned_to(:id => issue.assigned_to_id, :name => issue.assigned_to.name) unless issue.assigned_to.nil? api.category(:id => issue.category_id, :name => issue.category.name) unless issue.category.nil? api.fixed_version(:id => issue.fixed_version_id, :name => issue.fixed_version.name) unless issue.fixed_version.nil? api.parent(:id => issue.parent_id) unless issue.parent.nil? api.subject issue.subject api.description issue.description api.start_date issue.start_date api.due_date issue.due_date api.done_ratio issue.done_ratio api.is_private issue.is_private api.estimated_hours issue.estimated_hours # ここに追記したりする render_api_custom_values issue.visible_custom_field_values, api api.created_on issue.created_on api.updated_on issue.updated_on api.closed_on issue.closed_on api.array :relations do issue.relations.each do |relation| api.relation(:id => relation.id, :issue_id => relation.issue_from_id, :issue_to_id => relation.issue_to_id, :relation_type => relation.relation_type, :delay => relation.delay) end end if include_in_api_response?('relations') end end end
あとは空気を読んで生やすだけ,このrsbって仕様よく知らないんですけどまあ気合で.
あとはplugin入れるだけですね.かんたん.
今回のハマり
plugin名(init.rbに書くやつ)とpluginとして配置したときのディレクトリ名が異なるとうまく動かない.気をつけよう.
ActiveResourceでRedmine REST使ってみた
今更だけどActiveResource使ってみた.
ActiveResourceっていうのは,Railsのレールに完全に乗っかったモデルがあってREST APIが定義されていたら,別RailsアプリからでもActiveRecordみたいに触れる機能らしい.
ActiveResource腰にRedmineを触ることがあったのでメモがてらまとめ.
参考サイトは以下
使い方
今回はRails5でためす,Rails5で使う際には,masterのを使う必要があるみたいなのでGemfileに指定.
gem 'activeresource', github: 'rails/activeresource', branch: 'master'
参考サイト通りRedmineとつなげる.
class RedmineBase < ActiveResource::Base self.site = 'https://redmine.example' self.headers['X-Redmine-API-Key'] = 'hogee' self.format = :xml end
あとは,同名のモデルを作ればいい(migrateなどは不要なのでおもむろにmodelを追加する形でもいい)
class Project < RedmineBase end Project.find(:all)
ここで罠がある.
unicornみたいな複数スレッドを回すパターンだと何故か self.headers[X-Redmine-API-Key']
の中身が消失し,認証に失敗し続けるという事があったので,何とかして回避させる.
class RedmineBase < ActiveResource::Base self.site = 'https://redmine.example' self.headers['X-Redmine-API-Key'] = 'hogee' self.format = :xml def self.configure self.site = 'https://redmine.example' self.headers['X-Redmine-API-Key'] = 'hogee' self.format = :xml end end
こうした上で,application_controller.rb
に
class ApplicationController < ActionController::Base protect_from_forgery with: :exception before_action :configure_redmine_base private def configure_redmine_base RedmineBase.configure end end
こうやって書くと毎回セットアップされるようになるので良さそう.
モデル名とRESTのPathが一致しない場合には,prefix
を設定する.
class Membership < RedmineBase def self.set_project(project_id) self.prefix = "/projects/#{project_id}/" end end
Project.find(:all)
で一度に取れるプロジェクト数などがxml apiの関係でデフォルト25件になっている.*1
全件取りたいときはこんな感じ
@projects = [] page_index = 1 loop do projects = Project.find(:all, params: {page: page_index}) page_index = page_index + 1 break if projects.count == 0 @projects = @projects.concat(projects) end
limit
は最大100件なので,paramsにlimit: 100とすればもう少しリクエストを少なくできる.クエリーにつけるオプションなどもparamsに引っ付ければいい.
使ってみて
whereなどが使えないのでActiveRecordかと思いきやそうでもないので意外と使いにくい.今回の利用では問題なかったけどこれで思いっきり何かをするというのは意外と使えなさそう. Rails本家からパージされて別プロジェクトとかで管理されているので今後どうなるかわからない(純粋に分離しただけ説もありますが)
全件取るときなどは非常に時間がかかるので,結局mysqlに直繋ぎしたほうが早そうという気持ちもある.
罠
Project.findでIssue(この場合はチケット)で取れるカラムと直接Issue.findで取れるカラムが少し違うことがあって困った.今回はredmine pluginを作って必要なカラムを足すことで回避した.
*1:allってついているのにややこしいな...
LINE bot echoするやつ書いた
n番煎じ感あるけど書いたので一応.ハマリポイントの共有にちょうど良さそう.
コード
環境変数にBotのChannel IDとかMIDとか突っ込むと良い.
herokuにデプロイしてFixieというherokuからProxyして固定IPゲットするaddonを使う必要がある.
ハマりポイント
Callback URL問題
なんでや!と思ったら :443
が必要らしい.
https://URL:443/callback
みたいにしないといけない.
SSL問題
最初自分のVPSでやろうとして,HTTPS必要らしいのでLet's encryptしてみた.
ところが,一向にアクセスが来ない.あれれ〜おかしいぞ〜と思いHerokuにデプロイするとちゃんとアクセス来た.
Let's encryptがvaildな証明書じゃないらしい.めんどくせえ...
X_LINE_CHANNELSIGNATURE問題
正しいLineからのreceiveかどうかのvalidationチェックをするにあたり,HTTP headerを見る必要がある.
LINE Developers - BOT API - Getting started with BOT API Trial
ここを参考に X_LINE_CHANNELSIGNATURE
を見てたんだけど,なんか空文字であれ〜ってなってた.
よく見ると HTTP_X_LINE_CHANNELSIGNATURE
というヘッダーだった...ドキュメントェ...
channel_secret = ENV['CHANNEL_SECRET'] http_request_body = request.body.read hash = OpenSSL::HMAC::digest(OpenSSL::Digest::SHA256.new, channel_secret, http_request_body) signature = Base64.strict_encode64(hash) x_line_channelsignature = request.env["HTTP_X_LINE_CHANNELSIGNATURE"] if signature == x_line_channelsignature ...
(追記)
これもしかしたらSinatraが勝手に HTTP_
ってつけてるかもしれない.
Heroku固定IP問題
echoするためにSend API叩く必要があるんだけど,その際にWhitelistにIPを登録しないといけないみたい.
Herokuには固定IPがないので,FixieというaddonでProxy作って固定IPゲットして登録した.
BotkitでSlack Botを作りつつ自己アップデートできるようにした
Slack皆さん使ってますか?
Slackはただチャットが出来るだけではなく豊富なAPIによってもたらされる便利Botたちの存在が非常に大きいです.
Botを開発する際には,Githubが開発しているHubotが有名ですが,ここ最近ではSlack botに特化した(別に特化しているわけでもなさそう)BotkitというBotフレームワークが出ました.
Botの設置自体はリポジトリのREADME.mdを見ればわかるので,見てください. で,自己アップデートというのは何かって言うと,Botのコードを書き換えてpushした後に設置しているサーバにログインして,pullしてrestartするっていう作業がめっちゃだるかったので,自分で自分を更新して再起動させる仕組みを作りました.
基本的なコンセプトは HubotでHubotの更新をforeverを利用してHubotにさせてみる - MANA-DOT ここから得ました.ありがとうございます.
foreverでの起動
まず,参考ページの通りforeverでの起動をします. 元リポジトリの起動方法だと,tokenを環境変数で与えるようになっていますが,foreverでの環境変数の与え方がわからなかったので,Botのソース内に直書きするかファイルを読む仕組みにしましょう.
forever start bot.js
これで起動はできます.
参考ページにもある通り,foreverは死んでもうまいこと生き返らせてくれる機能を持っているので,アップデート処理が終わったら自殺するというコードをかけば良いでしょう.
アップデート処理
まずはコードを出します.
var child_process = require('child_process'); function updateSelf(bot, message){ child_process.exec('git reset --hard origin/master', function(error, stdout, stderr){ bot.reply(message, 'Botが更新されました!'); bot.reply(message, 'Botを再起動します'); setTimeout(function(){ process.exit(); }, 2000); }); } controller.hears(['update'], 'direct_mention', function(bot, message) { bot.reply(message, 'Botのアップデートを開始します'); child_process.exec('git fetch', function(error, stdout, stderr){ child_process.exec('git log master..origin/master', function(error, stdout, stderr){ if(stdout == ""){ bot.reply(message, 'Botは最新です'); }else{ bot.startConversation(message, function(error, convo){ bot.reply(message, '更新内容は以下のとおりです'); bot.reply(message, '```\n' + stdout + '\n```'); convo.ask('アップデートを行いますか?(y/n)', [ { pattern: bot.utterances.yes, callback: function(response, convo){ updateSelf(bot, message); convo.next(); } }, { pattern: bot.utterances.no, callback: function(response, convo){ bot.reply(message, 'アップデートを中止します'); convo.next(); } }, { pattern: 'じゃあそれで', callback: function(response, convo){ updateSelf(bot, message); convo.next(); } }, { default: true, callback: function(response, convo){ convo.repeat(); convo.next(); } } ]); }); } }); }); });
まずgitを操作したりするために,コマンド実行可能な child_process
を利用します.
update
コマンドが発行された時にまず,git fetchを行い,アップデートの確認を行います.
アップデートがあるのであれば,なんらかの標準出力がなされるので,それでアップデートがあるかどうかを判定します.
アップデートがある場合は,変更点を git log
で出力するようにしました.
また,BotkitのConversation機能を用いて,アップデートを行うかどうかの確認を行う仕組みを導入しました.
アップデートが許可された場合には, git reset --hard origin/master
とすることで,origin/master
への追従を行い,最新版へのコード更新が行われます.
その後, process.exit()
を発行し,自殺した後foreverによって再び生かされることになります.
process.exit()
を発行するときの setTimeout
が無いと,bot.reply()
が間に合わず何も出力されないケースがあるので,適当に時間とって setTimeout
した方がいいです.
実行時の様子
終わり
これでBot開発が捗る.
記事を自分で読み返して, git fetch
しておいて,アップデートを了承しなかった場合,もう一度updateした時には,fetchが働かず永久にupdateできないというバグを見つけてしまった. アップデートがあるかどうかの判定を git log master..origin/master
でやれば良さそう.