109 lines
2.3 KiB
Go
109 lines
2.3 KiB
Go
package slack
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type WebhookClient struct {
|
|
URL string
|
|
Timeout time.Duration
|
|
}
|
|
|
|
type block struct {
|
|
Type string `json:"type"`
|
|
Text *blockText `json:"text,omitempty"`
|
|
}
|
|
|
|
type blockText struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
type payload struct {
|
|
// Slackはblocksがあってもtextを求めるケースがあるので fallback 用に入れておく
|
|
Text string `json:"text,omitempty"`
|
|
Blocks []block `json:"blocks,omitempty"`
|
|
}
|
|
|
|
func (c WebhookClient) Send(title, text string) error {
|
|
if c.URL == "" {
|
|
return fmt.Errorf("webhook URL is empty")
|
|
}
|
|
if c.Timeout <= 0 {
|
|
c.Timeout = 10 * time.Second
|
|
}
|
|
|
|
p := makePayload(title, text)
|
|
|
|
body, err := json.Marshal(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPost, c.URL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
res, err := (&http.Client{Timeout: c.Timeout}).Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
b, _ := io.ReadAll(io.LimitReader(res.Body, 2048))
|
|
return fmt.Errorf("slack webhook failed: status=%d body=%s", res.StatusCode, string(bytes.TrimSpace(b)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func makePayload(title, text string) payload {
|
|
title = strings.TrimSpace(title)
|
|
text = strings.TrimSpace(text)
|
|
|
|
if title == "" {
|
|
// blocks なしで普通に送る
|
|
return payload{Text: text}
|
|
}
|
|
|
|
// blocks で「太字タイトル + 本文」
|
|
blocks := []block{
|
|
{
|
|
Type: "section",
|
|
Text: &blockText{
|
|
Type: "mrkdwn",
|
|
Text: "*" + escapeMrkdwn(title) + "*",
|
|
},
|
|
},
|
|
{
|
|
Type: "section",
|
|
Text: &blockText{
|
|
Type: "mrkdwn",
|
|
Text: escapeMrkdwn(text),
|
|
},
|
|
},
|
|
}
|
|
|
|
// fallback text はtitle + newline + body にしておく
|
|
return payload{
|
|
Text: title + "\n" + text,
|
|
Blocks: blocks,
|
|
}
|
|
}
|
|
|
|
// 最小限のエスケープ( _ ` などはそのまま使いたい人もいるので控えめ)
|
|
// 必要なら後で強化できます。
|
|
func escapeMrkdwn(s string) string {
|
|
// Slack mrkdwnは基本そのまま送れるので、ここは今は何もしない方が自然です。
|
|
// 将来的に必要になったらここで置換します。
|
|
return s
|
|
}
|