おみくじをしましょう
しましょう.
皆さんは各々個人slackをお持ちかと思いますので,ここにそんなこともあろうかと,うまく動くおみくじを用意いたしました.
動かし方
Deploy to Herokuをついでに配置したので使うと良さそう.
その後は,slackのoutgoing webhookで,
https://hogeee.heroku.com/omikuji
にoutgoingが飛ぶように設定
あとはslack使うだけ
今年もいい年になりそうですね
仕組み
package main import ( "log" "math/rand" "net/http" "os" "strings" "time" "github.com/gin-gonic/gin" ) func main() { port := os.Getenv("PORT") if port == "" { log.Fatal("$PORT must be set") } router := gin.Default() router.POST("/omikuji", func(c *gin.Context) { text := c.PostForm("text") log.Printf("%s", text) nakami := strings.Split(text, ",") rand.Seed(time.Now().UnixNano()) c.JSON(http.StatusOK, gin.H{ "text": nakami[rand.Intn(len(nakami))], "response_type": "in_channel", }) }) router.Run(":" + port) }
仕組みはかなりシンプルで,Outgoingでアクセスが来る→HTTP Responseの際に結果を返すだけ. 今回はシンプルにするために,カンマ区切りでシャッフルするようにした.
最初のos.GetEnv周りはHerokuでうまく動かすためのおまじない.slackのoutgoingはtextというパラメータにテキストが入っているのでそれを利用すればok.
ちなみに
この程度のシンプルなやつならslackbotでもできる.
去年頑張ってbot書いたら,この設定を後輩がしてて悲しい気持ちになったのでその供養をさせていただきました.
今年も頑張りましょう
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ゲットして登録した.