ファイルを追加
This commit is contained in:
parent
72407f2c8d
commit
8b57a363b1
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,3 +25,6 @@ go.work.sum
|
||||
# env file
|
||||
.env
|
||||
|
||||
# build artifacts
|
||||
bin/
|
||||
dist/
|
||||
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"golang.go"
|
||||
]
|
||||
}
|
||||
2
Makefile
2
Makefile
@ -1,4 +1,4 @@
|
||||
App=skacksend
|
||||
App=slacksend
|
||||
|
||||
.PHONY: fmt vet test build run check
|
||||
|
||||
|
||||
15
README.md
15
README.md
@ -11,6 +11,7 @@ Slackにメッセージを送信するツール
|
||||
- [Aiuto Docs](#aiuto-docs)
|
||||
- [Markdown](#markdown)
|
||||
- [Webserver](#webserver)
|
||||
- [Deploy](#deploy)
|
||||
|
||||
|
||||
## Environments
|
||||
@ -82,3 +83,17 @@ go install golang.org/x/pkgsite/cmd/pkgsite@latest
|
||||
pkgsite
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
```sh
|
||||
make build
|
||||
# go build -o bin/slacksend ./cmd/slacksend
|
||||
```
|
||||
|
||||
デバッグ情報を削ってビルドする場合
|
||||
|
||||
```sh
|
||||
go build -o bin/slacksend \
|
||||
-ldflags "-s -w" \
|
||||
./cmd/slacksend
|
||||
```
|
||||
|
||||
@ -10,7 +10,6 @@ func hello() string {
|
||||
return "hello"
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
os.Exit(cli.Run(os.Args))
|
||||
}
|
||||
|
||||
@ -6,7 +6,9 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.pglikers.com/tools/slacksend/internal/slack"
|
||||
"gitea.pglikers.com/tools/slacksend/internal/version"
|
||||
)
|
||||
|
||||
@ -14,6 +16,7 @@ type Options struct {
|
||||
ShowVersion bool
|
||||
WebhookURL string
|
||||
TimeoutSec int
|
||||
Title string
|
||||
}
|
||||
|
||||
func Run(args []string) int {
|
||||
@ -31,7 +34,7 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
// other options
|
||||
fs.StringVar(&opt.WebhookURL, "webhook", "", "Slack Incoming Webhook URL (or env SLACK_WEBHOOK_URL)")
|
||||
fs.IntVar(&opt.TimeoutSec, "timeout", 10, "HTTP timeout seconds")
|
||||
|
||||
fs.StringVar(&opt.Title, "title", "", "message title (shown as bold)")
|
||||
|
||||
if err := fs.Parse(args[1:]); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
@ -48,6 +51,12 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
if len(fs.Args()) == 0 && isTerminal(stdin) {
|
||||
fmt.Fprintln(stderr, "message is required. provide args or pipe via stdin")
|
||||
Usage(stderr)
|
||||
return 2
|
||||
}
|
||||
|
||||
// webhook URL の解決: flag > env
|
||||
webhookURL := strings.TrimSpace(opt.WebhookURL)
|
||||
if webhookURL == "" {
|
||||
@ -68,8 +77,17 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
return 2
|
||||
}
|
||||
|
||||
// TODO: Slack送信(ここはあとで internal/slack に逃がす)
|
||||
fmt.Fprintln(stdout, msg)
|
||||
client := slack.WebhookClient{
|
||||
URL: webhookURL,
|
||||
Timeout: time.Duration(opt.TimeoutSec) * time.Second,
|
||||
}
|
||||
|
||||
if err := client.Send(opt.Title, msg); err != nil {
|
||||
fmt.Fprintln(stderr, "send failed:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
fmt.Fprintln(stdout, "ok")
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -84,3 +102,17 @@ func message(args []string, stdin io.Reader) (string, error) {
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
}
|
||||
|
||||
// isTerminal reports whether the given reader is a character device (e.g., TTY).
|
||||
func isTerminal(r io.Reader) bool {
|
||||
fd, ok := r.(interface {
|
||||
Stat() (os.FileInfo, error)
|
||||
})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
info, err := fd.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.Mode()&os.ModeCharDevice != 0
|
||||
}
|
||||
|
||||
@ -13,6 +13,8 @@ Usage:
|
||||
echo "message" | slacksend [options]
|
||||
|
||||
Options:
|
||||
--title <title> Message title (shown as bold)
|
||||
--webhook <url> Slack Incoming Webhook URL
|
||||
-V, --version show version
|
||||
-h, --help show this help
|
||||
|
||||
|
||||
108
internal/slack/webhook.go
Normal file
108
internal/slack/webhook.go
Normal file
@ -0,0 +1,108 @@
|
||||
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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user