もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

write-blog-every-weekの通知ロジックをPythonからGoに書きなおした

はじめに

こんばんは。

絶賛Go勉強中の僕です。

前回・前々回と基礎的なところを勉強してみたので、今回は現状Pythonで実装しているブログSlackの通知ロジックをGoに書き直してみようと思いました。

その中でいろいろと学んだことをさらに記録しておこうと思います。

コード

設定ファイルどうする?

pythonで実装していたときは、YAMLファイルで用意してたのですが、Goはどうなのかなといろいろ調べてると、TOMLで用意するという記事がみかけたので、それに習ってみました。

インストール
$ go get github.com/BurntSushi/toml
config.toml
[Slack]
RequestToken    = "XXXXXX"
GetAPIURL       = "https://slack.com/api/channels.history"
GetChannelID    = "XXXXXX"
SendAPIURL      = "https://hooks.slack.com/XXXXXXX"
SendChannelName = "XXXXXX"

[[MemberData]]
BlogTitle = "もがき系プログラマの日常"
UserID    = "XXXXX"

[[MemberData]]
BlogTitle = "XXXXXXXXXXXXXXX"
UserID    = "XXXXX"
main.go
package main

import (
    "fmt"

    "github.com/BurntSushi/toml"
)

type ConfigData struct {
    Slack      Slack
    MemberData []MemberData
}

type Slack struct {
    RequestToken    string
    GetAPIURL       string
    GetChannelID    string
    SendAPIURL      string
    SendChannelName string
}

type MemberData struct {
    BlogTitle string
    UserID    string
}

func main() {
    var configData ConfigData
    _, err := toml.DecodeFile("config.toml", &configData)
    if err != nil {
        panic("tomlファイルを読み込めません")
    }

    fmt.Println(configData.Slack.GetAPIURL)
    fmt.Println(configData.Slack.RequestToken)

    for i := 0; i < len(configData.MemberData); i++ {
        fmt.Println(configData.MemberData[i].BlogTitle)
    }
}g
実行
$ go run main.go
https://slack.com/api/channels.history
XXXXXX
もがき系プログラマの日常
XXXXXXXXXXXXXXX

日付の計算とかどうする?

月曜日基準で考えて、水曜・金曜・日曜にリマインド通知を行っているので、それぞれの曜日で月曜までの時間の差を求めるみたいなことをしていました。

その求めた時間を、slackの発言履歴の時間の範囲指定で使うためです。

Pythonでは以下みたいなコードで対象の日付を取得してました。

def execute():
    nowDate    = GetNowDate()
    thisMonday = getThisMonday(nowDate.replace(hour=0)) #開始日は0時から取得したいので、

def getWeekDayNumber():
    u"""
    曜日の番号を返す
    @return int
    """
    return datetime.date.today().weekday()

def GetNowDate():
    u"""
    現在の日付を取得
   15時に通知するためhourは決め打ち
    @return string
    """
    return datetime.datetime.now().replace(hour=15, minute=0, second=0, microsecond=0)
    
def getThisMonday(nowDate):
    u"""
    今週の月曜の日付を取得
    @return string
    """
    weekday = getWeekDayNumber()
    return nowDate + datetime.timedelta(days=-weekday)

Goではいろいろ調べて以下みたいな感じにしました。

func main() {
    nowDate := GetNowDate()
    thisMonday := GetThisMonday(nowDate)

    fmt.Println("現在の日付 => " + nowDate.String())
    fmt.Println("月曜の日付 => " + thisMonday.String())
}

/**
 * 曜日の番号を返す
 * Goの場合
 * 0 => 日曜
 * 6 => 土曜
 *
 * になるので、Pythonにあわすため、以下にする
 * 0 => 月曜
 * 6 => 日曜
 */
func GetWeekDayNumber() int {
    weekday := int(time.Now().Weekday()) - 1
    if weekday == -1 {
        weekday = 6
    }

    return weekday
}

/**
 * 現在の日付を取得する
 */
func GetNowDate() time.Time {
    t := time.Now()
    return time.Date(t.Year(), t.Month(), t.Day(), 15, 00, 00, 0, time.Local)
}

/**
 * 今週の月曜の日付を取得する
 */
func GetThisMonday(nowDate time.Time) time.Time {
    weekday := GetWeekDayNumber()
    nowDate = time.Date(nowDate.Year(), nowDate.Month(), nowDate.Day(), 00, 00, 00, 0, time.Local)
    return nowDate.Add(time.Duration(-24*weekday) * time.Hour)
}
$ go run main.go
現在の日付 => 2018-10-14 15:00:00 +0900 JST
月曜の日付 => 2018-10-08 00:00:00 +0900 JST

URLエンコードどうする?

net/urlパッケージの Encode関数で再現できました。

func sendMessageAPI(configData ConfigData, latest time.Time, oldest time.Time) {
    params := url.Values{}
    params.Set("token", configData.Slack.RequestToken)
    params.Add("channel", configData.Slack.GetChannelID)
    params.Add("latest", strconv.FormatInt(latest.Unix(), 10))
    params.Add("oldest", strconv.FormatInt(oldest.Unix(), 10))
    params.Add("count", "700")
    fmt.Println(params.Encode())
}
$ go run main.go
channel=XXXXXX&count=700&latest=1539496800&oldest=1538924400&token=XXXXXX

また、ここを調べてるところで、

int64 => stringへのキャスト方法は、いつものstrconv.Atoi()ではなく、strconv.FormatInt()である

ということも学びました。第2引数の10は進数とのことです。

jsonのデータをデコードしたい

slackのchannel.historyAPIを実行し、結果のjsonを配列にしてpythonでは処理してました。

goではどうするのかなと調べてると、構造体とかに保存する必要があるみたいです。。

うぇぇぇ。。と思ってたら、jsonのデータをもとに構造体の定義を生成するサイトが合ったので、ソッコーで使いました。

JSON-to-Go

パッケージは encoding/json を使うようです。

type ChannelHistoriesData struct {
    Ok       bool   `json:"ok"`
    Latest   string `json:"latest"`
    Oldest   string `json:"oldest"`
    Messages []struct {
        Type        string `json:"type"`
        User        string `json:"user,omitempty"`
        Text        string `json:"text"`
        ClientMsgID string `json:"client_msg_id,omitempty"`
        ThreadTs    string `json:"thread_ts,omitempty"`
        Ts          string `json:"ts"`
        Username    string `json:"username,omitempty"`
        Icons       struct {
            Image36 string `json:"image_36"`
            Image48 string `json:"image_48"`
            Image72 string `json:"image_72"`
        } `json:"icons,omitempty"`
    } `json:"messages"`
    HasMore bool `json:"has_more"`
}

/**
 * ChannelHistoryAPIを実行する
 */
func sendChannelHistoryAPI(configData ConfigData, latest time.Time, oldest time.Time) {
    // パラメータをURLエンコードしたものを取得
    params := url.Values{}
    params.Set("token", configData.Slack.RequestToken)
    params.Add("channel", configData.Slack.GetChannelID)
    params.Add("latest", strconv.FormatInt(latest.Unix(), 10))
    params.Add("oldest", strconv.FormatInt(oldest.Unix(), 10))
    params.Add("count", "200")
    urlParams := params.Encode()

    // channel history apiを実行する
    response, httpRequestError := http.Get(configData.Slack.GetAPIURL + "?" + urlParams)
    if httpRequestError != nil {
        panic("sendChannelHistoryAPIのリクエストに失敗しました。")
    }
    defer response.Body.Close()

    // responseBodyを最終的に配列へ変換する
    byteData, _ := ioutil.ReadAll(response.Body)
    data := new(ChannelHistoriesData)
    jsonDecodeError := json.Unmarshal(byteData, data)
    if jsonDecodeError != nil {
        panic("JSONのデコードに失敗しました。")
    }
 
    return data
}

配列内(Slice内)の重複データを削除したい

現状の通知プログラムでは、通知アプリが発言したデータを配列に入れていたので、同じことをしようと思ったのですが、phpでいうarray_unique()みたいなのは調べるとGoではないようだったので、参考サイト様をみつけたので、使わせていただきました。

https://ema-hiro.hatenablog.com/entry/20170712/1499785693

/**
 * Slice内に存在する重複した値を削る
 */
func uniqueSlice(data []string) []string {
    m := make(map[string]bool)
    results := []string{}
    for _, ele := range data {
        if !m[ele] {
            m[ele] = true
            results = append(results, ele)
        }
    }

    return results
}

一旦trueを入れておいて、次のループでtrueが入ってたら、if文に入らず結果uniqueになるということです。

配列を文字列で結合したい

strings パッケージの、Joinで出来ました。

/**
 * Slackへ送信する用のメッセージを作成する
 */
func MakeNormalSendText(configData ConfigData, messageData []string) string {
    targetUsers := getTargetUsers(configData, messageData)
    textData := "<!channel>\nまだブログを書けていないユーザーがいます!\n今週中に書けるようみんなで煽りましょう!\n書けていないユーザー\n================\n" + strings.Join(targetUsers, "\n")

    return textData
}

終わりに

とりあえず移植は成功しました。

ただ、こんな夜中にテストで通知を飛ばしてしまって、すぐ消しましたが、ご迷惑をおかけしました。。。

f:id:kojirooooocks:20181014234759p:plain

あーまにあった。。。