LoginSignup
8
2

More than 3 years have passed since last update.

奉行クラウドのタイムレコーダー打刻を自動化するためのツールを作った

Last updated at Posted at 2021-02-07

Motivation

弊社の出勤退勤では奉行クラウドを利用していますが、奉行クラウドは公式からデスクトップクライアントを提供していないのでタイムレコーダーの打刻のためにわざわざブラウザから奉行クラウドのページを開いて打刻のボタンを押す必要があります。
この作業が実に面倒くさいし外出/再入などは結構忘れる事が多いのでどうにかならんもんかなと思って自動化することにしました。

TL;TD

マルチプラットフォーム環境に対応したCLIで奉行クラウドで打刻できるClientを作成しました。
bugyo-client-go
こんな感じで使えます。

# 出勤
bugyoclient punchmark --type in

# 退出
bugyoclient punchmark --type out

# 外出
bugyoclient punchmark --type go

# 再入
bugyoclient punchmark --type return

Windowsのタスクスケジューラでログオン、シャットダウン時に動くようにしておけば
PC立ち上げたら出勤PCシャットダウンしたら退勤が実現できます。

cliとしてではなくgolangのlibraryとしても利用出来るようにClientを作成したのでCloudRunLambdaでも動かせます。これでslackと連携も出来る!

package main

import (
    "github.com/tomtwinkle/bugyo-client-go"
    "log"
)

func main() {
    config := &bugyoclient.BugyoConfig{
        TenantCode: "<Your Tenant Code>",
        OBCiD: "<Your OBCID>",
        Password: "<Your Password>",
    }
    client, err := bugyoclient.NewClient(config)
    if err != nil {
        log.Fatal(err)
    }
    if err := client.Login(); err != nil {
        log.Fatal(err)
    }
    if err := client.Punchmark(bugyoclient.ClockTypeClockIn); err != nil {
        log.Fatal(err)
    }
}

まあそもそも打刻に奉行クラウド使ってないと使えないですけども!


以下には作成した際のログを残していくので奉行クラウド以外でも同じように公式でCLI提供されたないのでClientを作りたい場合に参考にしてください。

Bugyo Clientの作成

奉行クラウドにログインして打刻ボタン叩くまでSeleniumでやっても良いんですが
わざわざタイムレコーダーの打刻のためだけにSeleniumやヘッドレスブラウザ導入するのもtoo matchだしせっかくだからGoでClient作ることにします。

やることとしては単純、奉行クラウドのサイトにログインして打刻するまでのRequestをトレースしつつその間のsession情報を保持していればok。

http clientでSessionを保持する

Golangのbuilt-in packageであるhttpのclientはデフォルトではSessionを保持しません。
Session情報はcookieに記録されるのでhttp clientでsession保持させるためにcookiejarを用意してあげます。
どうでも良い話ですがクッキー入れておく瓶のことをクッキージャーって呼ぶんですね。

import (
    "net/http"
    "net/http/cookiejar"
)

type BugyoClient interface {}

type bugyoClient struct {
    client   *http.Client
}

func NewClient() (BugyoClient, error) {
    jar, err := cookiejar.New(nil)
    if err != nil {
        return nil, err
    }
    client := &http.Client{
        Jar: jar,
    }
    return &bugyoClient{client: client}, nil
}

あとは作成したclientを使い回せばClient側でSessionを保持してくれます。

referrerを設定する

ブラウザとしての動作を擬似的にトレースしてあげるためにはちゃんとreferrerの情報も渡してあげたほうが良いですね。
前回Requestの情報を元にreferrerを設定するようにしておきます。

func (b *bugyoClient) post(uri string, body url.Values) (*goquery.Document, error) {
    req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(body.Encode()))
    if err != nil {
        return nil, err
    }
    req.Header.Add("User-Agent", userAgent)
    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Add("__RequestVerificationToken", b.token)
    req.Header.Add("X-Requested-With", "XMLHttpRequest")
    if ref := b.refererForURL(b.lastReq); ref != "" {
        req.Header.Set("Referer", ref)
    }
    defer b.setLastReq(req.URL)

    // 省略
}

func (b *bugyoClient) refererForURL(lastReq *url.URL) string {
    if lastReq == nil {
        return ""
    }
    return lastReq.String()
}

func (b *bugyoClient) setLastReq(lastReq *url.URL) {
    b.lastReq = lastReq
}

ちなみにnet/http/clientに倣ってちゃんと書くと👇のようになりますが今回は接続先がhttps前提なのとauthは考えなくて良いので省略しています。

github.com/golang/go/blob/master/src/net/http/client.go#L144-L166
// refererForURL returns a referer without any authentication info or
// an empty string if lastReq scheme is https and newReq scheme is http.
func refererForURL(lastReq, newReq *url.URL) string {
    // https://tools.ietf.org/html/rfc7231#section-5.5.2
    //   "Clients SHOULD NOT include a Referer header field in a
    //    (non-secure) HTTP request if the referring page was
    //    transferred with a secure protocol."
    if lastReq.Scheme == "https" && newReq.Scheme == "http" {
        return ""
    }
    referer := lastReq.String()
    if lastReq.User != nil {
        // This is not very efficient, but is the best we can
        // do without:
        // - introducing a new method on URL
        // - creating a race condition
        // - copying the URL struct manually, which would cause
        //   maintenance problems down the line
        auth := lastReq.User.String() + "@"
        referer = strings.Replace(referer, auth, "", 1)
    }
    return referer
}

打刻Requestをトレースする

  • ログインまで
  1. トップページ表示 -> CSRF tokenを取得
  2. idの存在確認(CSRF tokenを利用)
  3. 認証実施(id, password, CSRF tokenを利用)
  4. 認証後リダイレクト(SessionIDを利用) -> user_codeを取得
  • 打刻まで
  1. 打刻ページに移動(SessionID, tenant_code, user_codeを利用)
  2. 打刻(clocktypeの指定で出勤、退勤、外出、再入を選択)

の流れっぽいです。
打刻の際にPositionLatitude, PositionLongitudeを渡しているんですが会社の場所を入力するのが正なのかなと思いつつ、実際のRequestでは適当な場所渡してるので取り敢えず適当な値でも動くみたいですね。

Bugyo ClientのCLI化

CLI化するにあたって利用したのはurfave/cliです。

  • app --version でversion情報出して欲しい
  • app --help でhelp出して欲しい

みたいな基本的なことから

  • app command [command options] のようにcommand毎にflagを設定したい
  • 特定コマンドで必須のフラグを指定していない場合はそのコマンドのhelpを出したい
  • shortnameを指定してcommandやflagを省略出来るようにもしたい

みたいな事が簡単に書けるので便利です。
今回は

bugyoclient punchmark --type <clock type>
もしくは
bugyoclient pm -t <clock type>

で打刻。

bugyoclient punchmark --type <clock type> --verbose

で詳細ログもセットで出力。

のようなパラメータ指定でCLIが実行出来るようしました。

    app := cli.NewApp()
    app.Name = "Bugyo Client CLI for Go"
    app.Usage = "奉行クラウドCLI"
    app.Author = "tomtwinkle"
    app.Version = fmt.Sprintf("bugyo-client-go cli version %s.rev-%s", version, revision)
    app.Commands = []cli.Command{
        {
            Name:      "punchmark",
            ShortName: "pm",
            Usage:     "タイムレコーダーの打刻を行う",
            Flags: []cli.Flag{
                cli.StringFlag{
                    Name: "type, t",
                    Usage: "出勤: --type in or -t in" +
                        "\n\t退出: --type out or -t out" +
                        "\n\t外出: --type go or -t go" +
                        "\n\t再入: --type return or -t return",
                    Required: true,
                    Value:    "",
                },
                cli.BoolFlag{
                    Name:     "verbose, v",
                    Usage:    "--verbose or -v",
                    Required: false,
                },
            },
            Action: func(c *cli.Context) error {
                var bcli bugyocli.CLI
                if c.Bool("verbose") {
                    bcli = bugyocli.NewCLI(true)
                } else {
                    bcli = bugyocli.NewCLI(false)
                }
                switch c.String("type") {
                case "in":
                    return bcli.PunchMark(bugyoclient.ClockTypeClockIn)
                case "out":
                    return bcli.PunchMark(bugyoclient.ClockTypeClockOut)
                case "go":
                    return bcli.PunchMark(bugyoclient.ClockTypeGoOut)
                case "return":
                    return bcli.PunchMark(bugyoclient.ClockTypeReturned)
                default:
                    return cli.ShowSubcommandHelp(c)
                }
            },
        },
    }
    if err := app.Run(os.Args); err != nil {
        log.Fatal(err)
    }

試しにhelpを出してみます。

$ bugyoclient.exe help
NAME:
   Bugyo Client CLI for Go - 奉行クラウドCLI

USAGE:
   bugyoclient.exe [global options] command [command options] [arguments...]

VERSION:
   bugyo-client-go cli version 0.10.6.rev-a83035a

AUTHOR:
   tomtwinkle

COMMANDS:
   punchmark, pm  タイムレコーダーの打刻を行う
   help, h        Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version
$ bugyoclient.exe punchmark
NAME:
   bugyoclient.exe punchmark - タイムレコーダーの打刻を行う

USAGE:
   bugyoclient.exe punchmark [command options] [arguments...]

OPTIONS:
   --type value, -t value  出勤: --type in or -t in
                           退出: --type out or -t out
                           外出: --type go or -t go
                           再入: --type return or -t return
   --verbose, -v           --verbose or -v

Required flag "type" not set

Flag定義くらいしかしてないですがで結構いい感じにhelpが出力されてますね。

go buildしたモジュールをgithubのreleasesに自動的に添付する

手動でgo buildしてgithubのreleasesに添付していくのは面倒なのでこれもCI/CDで自動化していきたい所です。
具体的にはtagで新規バージョンを切ってmaster burnch(今回はmain brunchですが)にpushした際に自動的にreleaseするように設定してきます。

これを実現するためにGithub ActionGoReleaserの組み合わせで実行します。

GoReleaserの設定

まずはGoReleaserの設定をしていきます。
プロジェクトルートに.goreleaser.ymlを作成します。

.goreleaser.yml
project_name: bugyoclient
env:
  - GO111MODULE=on
before:
  hooks:
    - go mod tidy
builds:
  - main: ./cmd
    binary: bugyoclient
    goos:
      - windows
      - darwin
      - linux
    goarch:
      - amd64
      - 386
    ldflags:
      - -s -w
      - -X main.version={{.Version}}
      - -X main.revision={{.ShortCommit}}
    env:
      - CGO_ENABLED=0
archives:
  - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
    files:
      - README.md
    format_overrides:
      - goos: windows
        format: zip
release:
  prerelease: auto
  • builds/mainにbuild対象のmain.goがあるエントリポイントのパスを記載
  • builds/goos, builds/goarchにbuild対象のプラットフォームを記載
  • builds/ldflagsにbuild時のリンカオプションを指定。今回はmain.goのグローバル変数の値の書き換え

    main.go
    
    var version = "unknown" // リンカオプションでbuild時に書き換え
    var revision = "unknown" // リンカオプションでbuild時に書き換え
    
    func main() {
    }
    
  • archives/name_template で出力するアーカイブ名の定義

  • archives/files でバイナリ以外にアーカイブに含めたいファイルの指定

  • archives/format_overrides で goos=windowsの場合のみzipで出力するように変更

Github Actionの設定

.github/workflows/release.yml を作成します。
github上からもhttps://github.com/<user>/<repository>/actions/newで作成出来ます。

.github/workflows/release.yml
name: release
on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"
jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v1
        with:
          fetch-depth: 1
      - name: Setup Go
        uses: actions/setup-go@v1
        with:
          go-version: 1.15
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v1
        with:
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  • on/push/tags でversion形式でtag pushされた時に動作するようにします
  • uses: goreleaser/goreleaser-action@v1でgoreleaserを先程作成した設定で動かします

適当な修正でtag pushしてみましたがこんな感じでbuild済みのバイナリが格納されたアーカイブが自動でReleasesのAssetsに添付されます。

image.png

完成品

というわけで出来上がったのがこちらです。
bugyo-client-go
奉行クラウドを使ってなきゃそもそも使えないので非常にニッチですが少なくとも社内の人には使ってもらえるかな。
READMEにまだWindows版しかInstallation書いてないですが特にプラットフォーム固有の処理は書いてないのでバイナリをDownloadすればUbuntuやmacでも動くはず。
動かなかったらissue上げといてください。

参考サイト

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2