2016年を振り返る

すでに2017年が来ましたが振り返ります.

記事にしたやつ

Nearbyについて

github.com

github.com

ログを見ると1/2に書いているらしいのでよほど暇だったのでしょう.これについては過去記事にも書きましたね.

pgmot.hatenablog.com

詳細についてそのうち書くみたいなことかいてますが,元気がなくかかれずに一年が経ってしまいました.おそらくコードも古いので一度書き直したほうがいいかもですね.

golangについて

2016年はgolangを頑張った気がします.頑張ったというか手を出したというか.入社後使うことはあるのかしら.

アクセスしてきたクライアントのグローバル側のIPとホストを出すサーバ github.com

Websocketでチャットをできるようにしたサーバ github.com

slackでおみくじできるようにしたサーバ github.com

画像のアップロードとそのホスティングをするサーバ github.com

golang結構書きやすくてよかった.一年に1つ新しい言語を学ぶ姿勢みたいなのがいいと聞いて始めたんだけど長い付き合いになりそう.

line botについて

github.com

pgmot.hatenablog.com

LINE Botがdeveloper previewとして出た次の日ぐらいに書いたbotです.line echoでlinechoという名前になっています. 次の日ぐらいに書いたんですけどやはり最初は取れなかったですね.取ったところで同しようもないのですが. ただ,めちゃくちゃバズってた同じようなLINE echo botの記事で,ここが引っかかるよ!みたいな説明をみて同じのに引っかかった記憶があり,自らの手での体験は重要であるなぁと思った.

それ以外

研究

Android JavaJavaScriptを書いてます.たまのツールでGolang

pgmot.hatenablog.com

お仕事

Rubyと,JavaScript書きました.

pgmot.hatenablog.com

pgmot.hatenablog.com

まとめと雑感

研究でJava書きまくるのはいつもどおりでした.2016年の試みとしては,GolangJavaScript(ReactとかES6とか)が新しいところになるのかな.2017年もこの調子で新しい言語をゆっくり覚えていきたい.

2017年は

Elixir勉強しています.ガンバルゾー

golangでparseのinstall数を集計するスクリプト書いた

書いた.

Parse.com使ってて(オンプレ移行は成功しそう),Parseに突っ込んでるログからインストール数の集計をできるようなコマンドを書いてみた.リポジトリに上げたいところなんだけどまだ,キーがハードコードされてるので治ったら.

要件

  • コマンド一つ集計可能
  • AndroidiOSでそれぞれカウントする
  • 月ごとの集計ができる

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.EachdmIos.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として月を利用することでなんとかしました.ただ問題があって月ごとの並び順がバラバラになりがちなのでなんとかしたい.

感想

jsonのパースが楽だと思いました. これgolangとして良い書き方なのかわからないので詳しい人教えてください.

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も試す.

参考

NSD 4 – 日本Unboundユーザー会

qiita.com

セカンダリDNSをNSDにしてみた | Aimless

RedmineのREST APIを拡張する話

RedmineREST APIは良く出来てるんだけど,このカラムがほしいみたいなときにはどうすればよいのか.

結構かんたんな話で,api viewを作ればいい.参考:

qiita.com

この通りに拡張したい*.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を触ることがあったのでメモがてらまとめ.

github.com

参考サイトは以下

qiita.com

使い方

今回は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番煎じ感あるけど書いたので一応.ハマリポイントの共有にちょうど良さそう.

コード

github.com

環境変数BotのChannel IDとかMIDとか突っ込むと良い.

herokuにデプロイしてFixieというherokuからProxyして固定IPゲットするaddonを使う必要がある.

ハマりポイント

Callback URL問題

f:id:programmerMOT:20160408121909p:plain

なんでや!と思ったら :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フレームワークが出ました.

github.com

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 した方がいいです.

実行時の様子

f:id:programmerMOT:20160226125551p:plain

終わり

これでBot開発が捗る.

記事を自分で読み返して, git fetch しておいて,アップデートを了承しなかった場合,もう一度updateした時には,fetchが働かず永久にupdateできないというバグを見つけてしまった. アップデートがあるかどうかの判定を git log master..origin/master でやれば良さそう.