読者です 読者をやめる 読者になる 読者になる

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 でやれば良さそう.

NearbyでAndroid,iOS通信(解説は元気が出たら)

久々にこっちで書く.

NearbyというのはGoogleが提供しているpub-sub型のメッセージ通信の仕組みで,BluetoothとかWi-Fiとか非可聴音をうまく使って近辺にある端末を探しだしいい感じに通信させてくれる仕組み. AndroidiOSで動くようになっていて,相互のプラットフォーム間での通信も簡単にできる.前にAndroid間での通信はしたんだけど,そういえばお布施を払わなくてもiOS実機試せると思い出し,Android <-> iOS通信してみることとした. 本当は気合をいれて解説するとばかうけなんだろうけど,眠いのでとりあえず後日.リポジトリと参考サイトの列挙でとどめておこうと思う.

リポジトリ

github.com

github.com

iOS版は気合入れてSwiftで書いてみた.ただゴミだと思うので,詳しい人教えてほしい. APIKeyは,Androidはプロジェクトルートにgoogle_api_key.txtという名前でファイルを置いておもむろにAPIkeyだけかけばいい. iOSソースコード中に書いてあるんのでいい感じに書き換えてくれ.

参考文献

Googleの公式ドキュメントとりあえず読むと良い. Overview  |  Nearby Messages API  |  Google Developers

AndroidでのNearby解説.記事内はKotlinで書かれているが自分はJavaで書いた. Android - Nearby Messages APIでチャットみたいなのを作ってみる - Qiita

Android ListViewに関してコードパクった頂きました.ありがとうございます. 灯火: Android : ListView が一番上 / 一番下にスクロールしたかを調べる

AndroidGoogle API Keyを外部ファイルとしていい感じに管理する方法.gradle便利だなぁ. Manage Google Maps API Key with Gradle in Android Studio - Stack Overflow

iOSでのNearby解説.本当にお世話になりました. Nearby Messages by Swift - ゆずとみかんといちご