Togglの記録をServerless + Pixelaで草化
- 作業時間などの時間管理ツールとしてTogglがある
- いつ,どの作業をしたかを記録
- 各作業をプロジェクトやタグで分類可能
- Toggl Reportsで可視化も提供されており,特定の作業をどれくらい継続しているか,どのくらい時間をかけているかを見れる
- でもとりあえず草化したい!
- ToggleはAPIを提供しているので比較的用意にデータ抽出可能
- 画像認識とかいらない!
作ったもの
- 1日1回,前日に特定プロジェクトにかけた時間をTogglから抽出し,Pixelaに記録
結果
- 自分の勉強時間を草化できた🌱🌱🌱
環境
- MacOS Mojave
- Go 1.11.1
- Serverless Framework 1.32.0
つまづきメモ
- しょぼい内容だが備忘録として
Lambdaにて時間を扱う場合の注意
- CloudWatch Eventsをcron式で時間指定する場合,UTCで指定すること
- e.g. JSTで毎日午前1時に実行したい→UTCで午後4時(-9時間)を指定する
cron( 0 16 * * ? * )
- e.g. JSTで毎日午前1時に実行したい→UTCで午後4時(-9時間)を指定する
- Lambda関数で日時を取得する場合(e.g. Goでの
time.Now()
),標準ではUTCで取得する - 日本時間を使いたい場合はLambda関数の環境変数でタイムゾーンを指定すること
- e.g. 変数
TZ
, 値Asia/Tokyo
- e.g. 変数
Toggl APIの使い方
- TogglのAPIを利用したい場合,リクエストにAPIトークンを含める
- APIトークンはProfileから取得可能: https://support.toggl.com/my-profile/
- 今回は特定期間の記録を全取得し,特定プロジェクトの記録のみ加算していき合計時間を取得
- 特定期間の記録を取得するAPIは以下
GET https://www.toggl.com/api/v8/time_entries?start_date=XXX&end_date=XXX
- 日時はISO 8601形式
- 今回はGoのwrapperであるdougEfresh/gtogglを利用
- READMEの記載内容だとうまく行かず
import "github.com/dougEfresh/gtoggl"
import "github.com/dougEfresh/gtoggl-api/gtproject"
func main() {
// HTTP client作成
thc, err := gthttp.NewClient("your-api-token")
...
// Togglの記録(time entry)取得用クライアント作成
tec := gttimeentry.NewClient(thc)
// 特定期間の記録を取得
entries, eerr := tec.GetRange(start_date, end_date)
}
開発詳細
- 作ったものはGitHub - jagijagijag1/toggl2pixelaで公開
Serverless framework + Goで開始
-
$GOHOME/src
配下で作業
$ serverless create -t aws-go-dep -p <project-name>
- 東京リージョンにデプロイしたいので
serverless.yml
にregion
を追記
serverless.yml
provider:
name: aws
runtime: go1.x
region: ap-northeast-1
- 以下でひとまずデプロイテスト可能
$ cd <project-name>
$ make
$ sls deploy
新規関数を作成
- 関数を新規作成
- 自動生成された関数は不要なので削除
-
toggl2pixela
フォルダを作成し,main.go
を作成 -
Makefile
のbuild:
に以下を追記
Makefile
env GOOS=linux go build -ldflags="-s -w" -o bin/toggl2pixela toggl2pixela/main.go
serverless.yml
の修正
-
serverless.yml
の主な修正・追記点は以下- 新規作成した関数定義の追記 (+自動生成された関数定義の削除)
-
events
下にschecule: ***
を書くことで定期実行を定義 (下記では毎日午前1時に実行,上述の通りcron式の時間はUTC指定なので注意) - Lambda関数でJSTで日時取得したいので,環境変数
TZ
, 値Asia/Tokyo
を指定 - Lambda関数の環境変数(
environment
)にTogglのAPIキー/対象プロジェクトID,Pixelaのユーザ/トークン/グラフ情報を与える
serverless.yml
service: toggl2pixela
frameworkVersion: ">=1.28.0 <2.0.0"
provider:
name: aws
runtime: go1.x
region: ap-northeast-1
package:
exclude:
- ./**
include:
- ./bin/**
functions:
toggl2pixela:
handler: bin/toggl2pixela
events:
- schedule: cron(0 16 * * ? *)
# you need to fill the followings with your own
environment:
TZ: Asia/Tokyo
TOGGL_API_TOKEN: <your-api-token>
TOGGL_PROJECT_ID: <target-project-id>
PIXELA_USER: <user-id>
PIXELA_TOKEN: <your-token>
PIXELA_GRAPH: <your-graph-id-1>
timeout: 10
関数本体を作成
- 素直に実装しただけなので,特記事項なし…
- データ元のToggl,データ投入先のPixelaの情報は環境変数(
TOGGL_API_TOKEN, TOGGL_PROJECT_ID, PIXELA_USER, PIXELA_TOKEN, PIXELA_GRAPH
)から取得 - GoでのToggl操作にはdougEfresh/gtogglを利用
- 利用方法は上述
- GoでのPixela操作にはgainings/pixela-go-clientを利用
- データ元のToggl,データ投入先のPixelaの情報は環境変数(
toggl2pixela/main.go
package main
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"time"
"github.com/aws/aws-lambda-go/lambda"
"github.com/dougEfresh/gtoggl-api/gthttp"
"github.com/dougEfresh/gtoggl-api/gttimentry"
pixela "github.com/gainings/pixela-go-client"
)
// Handler is our lambda handler invoked by the `lambda.Start` function call
func Handler(ctx context.Context) error {
// extract env var
apiToken := os.Getenv("TOGGL_API_TOKEN")
pjID, _ := strconv.ParseUint(os.Getenv("TOGGL_PROJECT_ID"), 10, 64)
user := os.Getenv("PIXELA_USER")
token := os.Getenv("PIXELA_TOKEN")
graph := os.Getenv("PIXELA_GRAPH")
// extract data from toggl
date, quantity := getDateAndTimeFromToggl(apiToken, pjID)
if date == "-1" || quantity == "-1" {
return errors.New("Error in accessing toggl")
}
fmt.Printf("date: %s, quantity: %s\n", date, quantity)
// record pixel
perr := recordPixel(user, token, graph, date, quantity)
if perr != nil {
return errors.New("Error in accessing pixela")
}
return nil
}
func getDateAndTimeFromToggl(apiToken string, pjID uint64) (string, string) {
// create toggl client
thc, err := gthttp.NewClient(apiToken)
if err != nil {
fmt.Println(err)
return "-1", "-1"
}
// set time range to be analyzed
y := time.Now().AddDate(0, 0, -1)
s := time.Date(y.Year(), y.Month(), y.Day(), 0, 0, 0, 0, time.Local)
e := time.Date(y.Year(), y.Month(), y.Day(), 23, 59, 59, 0, time.Local)
date := y.Format("20060102")
// get time entries
total := int64(0)
tec := gttimeentry.NewClient(thc)
entries, eerr := tec.GetRange(s, e)
if eerr != nil {
fmt.Println(eerr)
return "-1", "-1"
}
// sum durations with project pjID
for _, e := range entries {
if e.Pid == pjID {
total += e.Duration
}
}
totalMin := float64(total) / 60
quantity := strconv.FormatFloat(totalMin, 'f', 4, 64)
return date, quantity
}
func recordPixel(user, token, graph, date, quantity string) error {
c := pixela.NewClient(user, token)
// try to record
err := c.RegisterPixel(graph, date, quantity)
if err == nil {
fmt.Println("recorded")
return err
}
// if fail, try to update
err = c.UpdatePixelQuantity(graph, date, quantity)
if err == nil {
fmt.Println("updated")
}
return err
}
func main() {
lambda.Start(Handler)
}