GoとSlack Block Kitによるシンプルな通知スクリプト

Goの書籍を読んでいたら簡単なスクリプトぐらい書いてみたいと思って、めちゃくちゃシンプルなSlack通知のスクリプトを書いてみました

Slack Block Kit

SlackのUIフレームワークでブロックを積み重ねてレイアウトができるコンポーネントになります。

https://api.slack.com/block-kit

色々できることもありますが、滅茶苦茶シンプルにWebhookを使ってメッセージを送信することも可能なので今回はそれだけやってみます。

GoでSlack通知のスクリプト

今回はせっかくなので、良く忘れてしまう数ヶ月~年単位でしかやらない作業を思い出すためにリマインド通知をするものをつくりました

メンテナンス情報を格納した下記のようなjsonファイルを用意しておきます。

{
    "targets" : [
        {
            "name" : "サマータイヤの車の空気圧",
            "category" : "自動車",
            "description" : "車の空気圧を半年に1回入れ替える",
            "maintenance_interval" : 180,
            "last_updated": "2025-04-05T00:00:00Z"
        },
        {
            "name" : "リビングエアコンのフィルタ掃除",
            "category" : "掃除",
            "description" : "リビングエアコンのフィルタを定期的に掃除する",
            "maintenance_interval" : 180,
            "last_updated": "2025-04-05T00:00:00Z"
        }
    ]
}

これを読み込んで期限の近づいたものと過ぎたものでグルーピングしてメッセージを表示するというものです。

メッセージのためのBlockを動的に生成してから最後にまとめてメッセージとして発信するという流れです。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

type Maintenance struct {
	Name                string    `json:"name"`
	Category            string    `json:"category"`
	Description         string    `json:"description"`
	MaintenanceInterval int       `json:"maintenance_interval"`
	LastUpdated         time.Time `json:"last_updated"`
}

type Config struct {
	Maintenances []Maintenance `json:"targets"`
}
type SlackMessage struct {
	Blocks []Block `json:"blocks"`
}

type Block struct {
	Type   string       `json:"type"`
	Text   *TextObject  `json:"text,omitempty"`
	Fields []TextObject `json:"fields,omitempty"`
}

type TextObject struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

func (m Maintenance) IsWarningDue() bool {
	next := time.Time(m.LastUpdated).AddDate(0, 0, m.MaintenanceInterval-7)
	return time.Now().After(next)
}

func (m Maintenance) IsCriticalDue() bool {
	next := time.Time(m.LastUpdated).AddDate(0, 0, m.MaintenanceInterval)
	return time.Now().After(next)
}

func formatDate(t time.Time) string {
	return t.Format("2006-01-02")
}

func NewSlackMessage(title string, m []Maintenance) SlackMessage {
	var dynBlocks []Block
	for _, task := range m {
		block := Block{
			Type: "section",
			Fields: []TextObject{
				{Type: "mrkdwn", Text: fmt.Sprintf("*Title:*\n%s", task.Name)},
				{Type: "mrkdwn", Text: fmt.Sprintf("*Category:*\n%s", task.Category)},
				{Type: "mrkdwn", Text: fmt.Sprintf("*Description:*\n%s", task.Description)},
				{Type: "mrkdwn", Text: fmt.Sprintf("*LastUpdated:*\n%s", formatDate(task.LastUpdated))},
			},
		}
		dynBlocks = append(dynBlocks, block)
	}
	firstBlock := Block{
		Type: "section",
		Text: &TextObject{
			Type: "mrkdwn",
			Text: fmt.Sprintf("*%s* <!channel>\n下記のタスクを確認してください", title),
		},
	}
	allBlocks := append([]Block{firstBlock}, dynBlocks...)

	return SlackMessage{
		Blocks: allBlocks,
	}
}

func SendSlackMessage(webhookURL string, msg SlackMessage) error {
	payload, err := json.Marshal(msg)
	if err != nil {
		return fmt.Errorf("failed to marshal message: %w", err)
	}

	resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(payload))
	if err != nil {
		return fmt.Errorf("failed to post to Slack: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		return fmt.Errorf("received non-2xx response from Slack: %s", resp.Status)
	}

	return nil
}

func main() {
	webhookURL, exists := os.LookupEnv("SLACK_WEBHOOK")
	if !exists {
		fmt.Println("SLACK_WEBHOOK is not set")
		return
	}

	file, err := os.Open("data.json")
	if err != nil {
		fmt.Println("ファイルを開けません:", err)
		return
	}
	defer file.Close()

	var config Config
	decoder := json.NewDecoder(file)
	if err := decoder.Decode(&config); err != nil {
		fmt.Println("デコードエラー:", err)
		return
	}

	var warnings, criticals []Maintenance

	// 重要度に応じてタスクを分類する
	for _, task := range config.Maintenances {
		if task.IsCriticalDue() {
			criticals = append(criticals, task)
		} else if task.IsWarningDue() {
			warnings = append(warnings, task)
		}
	}

	if len(criticals) != 0 {
		SendSlackMessage(webhookURL, NewSlackMessage("Critical", criticals))
	}
	if len(warnings) != 0 {
		SendSlackMessage(webhookURL, NewSlackMessage("Warning", warnings))
	}
}

今回はサクッとやるためにIncoming Webhook を使っています

環境変数に定義する形で用意しているので、定期実行するならgithub actionsなどでsecretsにwebhookのURLだけ設定すれば動かせるはずです