Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f8b8490e8 | |||
| 8b57a363b1 | |||
| 72407f2c8d | |||
| 66fdf87fc0 | |||
| 71ea80bd4a | |||
| c49288194e |
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,3 +25,6 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.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
|
.PHONY: fmt vet test build run check
|
||||||
|
|
||||||
|
|||||||
109
README.md
109
README.md
@ -1,16 +1,50 @@
|
|||||||
# slacksend
|
# slacksend
|
||||||
|
|
||||||
Slackにメッセージを送信する
|
Slackにメッセージを送信するツール
|
||||||
|
|
||||||
|
- [slacksend](#slacksend)
|
||||||
|
- [Environments](#environments)
|
||||||
|
- [Functions](#functions)
|
||||||
|
- [How To Use](#how-to-use)
|
||||||
|
- [Develop](#develop)
|
||||||
|
- [Init](#init)
|
||||||
|
- [Aiuto Docs](#aiuto-docs)
|
||||||
|
- [Markdown](#markdown)
|
||||||
|
- [Webserver](#webserver)
|
||||||
|
- [Deploy](#deploy)
|
||||||
|
- [ローカルへインストールする](#ローカルへインストールする)
|
||||||
|
- [aptパッケージを作成する](#aptパッケージを作成する)
|
||||||
|
|
||||||
|
|
||||||
|
## Environments
|
||||||
|
|
||||||
|
Slack Appで`Incoming Webhook`を有効にする必要があります。
|
||||||
|
|
||||||
|
[Slack App](https://api.slack.com/apps)はこちらからどうぞ
|
||||||
|
|
||||||
|
* 参考記事: https://qiita.com/to3izo/items/c2d16f8b3e52b09e543e
|
||||||
|
|
||||||
## Functions
|
## Functions
|
||||||
|
|
||||||
* Slackのチャンネルにメッセージを送信する
|
* Slackのチャンネルにメッセージを送信する
|
||||||
* Incoming Webhookに対応
|
* Incoming Webhookに対応
|
||||||
|
* `SLACK_WEBHOOK_URL` : URLを指定する
|
||||||
|
* `--title`: タイトルを設定する
|
||||||
|
* 現在の仕様ではチャンネル設定はできません。
|
||||||
|
* 宛先によりチャンネルを設定します。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dev
|
## How To Use
|
||||||
|
|
||||||
|
Incoming Webhookでメッセージを送信する場合
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/XXX/YYY/ZZZ"
|
||||||
|
slacksend --title "タイトル" "本文を指定してください"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Develop
|
||||||
|
|
||||||
実行
|
実行
|
||||||
|
|
||||||
@ -25,15 +59,15 @@ go run ./cmd/slacksend
|
|||||||
go build -o slacksend ./cmd/slacksend
|
go build -o slacksend ./cmd/slacksend
|
||||||
```
|
```
|
||||||
|
|
||||||
## Init
|
### Init
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go mod init gitea.pglikers.com/tools/slacksen
|
go mod init gitea.pglikers.com/tools/slacksen
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docs
|
### Aiuto Docs
|
||||||
|
|
||||||
### Markdown
|
#### Markdown
|
||||||
|
|
||||||
`gomarkdoc`のInstallが必要です
|
`gomarkdoc`のInstallが必要です
|
||||||
|
|
||||||
@ -42,7 +76,7 @@ go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest
|
|||||||
gomarkdoc ./... > docs/api.md
|
gomarkdoc ./... > docs/api.md
|
||||||
```
|
```
|
||||||
|
|
||||||
### Webserver
|
#### Webserver
|
||||||
|
|
||||||
`pkgsite`のInstallが必要です
|
`pkgsite`のInstallが必要です
|
||||||
|
|
||||||
@ -51,3 +85,64 @@ go install golang.org/x/pkgsite/cmd/pkgsite@latest
|
|||||||
pkgsite
|
pkgsite
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make build
|
||||||
|
# go build -o bin/slacksend ./cmd/slacksend
|
||||||
|
```
|
||||||
|
|
||||||
|
デバッグ情報を削ってビルドする場合
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go build -o bin/slacksend \
|
||||||
|
-ldflags "-s -w" \
|
||||||
|
./cmd/slacksend
|
||||||
|
```
|
||||||
|
|
||||||
|
ビルド時にGit情報を埋め込む方法
|
||||||
|
|
||||||
|
```sh
|
||||||
|
VERSION=$(git describe --tags --always --dirty)
|
||||||
|
COMMIT=$(git rev-parse --short HEAD)
|
||||||
|
DATE=$(date +"%Y-%m-%d")
|
||||||
|
|
||||||
|
go build \
|
||||||
|
-ldflags "\
|
||||||
|
-X main.version=$VERSION \
|
||||||
|
-X main.commit=$COMMIT \
|
||||||
|
-X main.date=$DATE" \
|
||||||
|
-o slacksend \
|
||||||
|
./cmd/slacksend
|
||||||
|
```
|
||||||
|
|
||||||
|
### ローカルへインストールする
|
||||||
|
|
||||||
|
インストールする
|
||||||
|
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install ./cmd/slacksend
|
||||||
|
slacksend -v
|
||||||
|
# Goのルートを確認する
|
||||||
|
go env GOPATH
|
||||||
|
```
|
||||||
|
|
||||||
|
`~/go/bin`が`PATH`に入ってない場合は .zshrcに設定する
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vim ~/.zshrc
|
||||||
|
export PATH="$(go env GOPATH)/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
アンインストール方法
|
||||||
|
|
||||||
|
```sh
|
||||||
|
which slacksend
|
||||||
|
# パスにあるバイナリファイルを削除する
|
||||||
|
rm /Users/xxx/go/bin/slacksend
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### aptパッケージを作成する
|
||||||
|
|||||||
@ -10,7 +10,6 @@ func hello() string {
|
|||||||
return "hello"
|
return "hello"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
os.Exit(cli.Run(os.Args))
|
os.Exit(cli.Run(os.Args))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,4 +9,4 @@ func TestHello(t *testing.T) {
|
|||||||
if got != want {
|
if got != want {
|
||||||
t.Errorf("hello() = %q, want %q", got, want)
|
t.Errorf("hello() = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,12 +6,17 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.pglikers.com/tools/slacksend/internal/slack"
|
||||||
"gitea.pglikers.com/tools/slacksend/internal/version"
|
"gitea.pglikers.com/tools/slacksend/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
ShowVersion bool
|
ShowVersion bool
|
||||||
|
WebhookURL string
|
||||||
|
TimeoutSec int
|
||||||
|
Title string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(args []string) int {
|
func Run(args []string) int {
|
||||||
@ -26,6 +31,10 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
var opt Options
|
var opt Options
|
||||||
fs.BoolVar(&opt.ShowVersion, "version", false, "show version")
|
fs.BoolVar(&opt.ShowVersion, "version", false, "show version")
|
||||||
fs.BoolVar(&opt.ShowVersion, "V", false, "show version (short)")
|
fs.BoolVar(&opt.ShowVersion, "V", false, "show version (short)")
|
||||||
|
// 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 := fs.Parse(args[1:]); err != nil {
|
||||||
if err == flag.ErrHelp {
|
if err == flag.ErrHelp {
|
||||||
@ -42,6 +51,22 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
return 0
|
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 == "" {
|
||||||
|
webhookURL = strings.TrimSpace(os.Getenv("SLACK_WEBHOOK_URL"))
|
||||||
|
}
|
||||||
|
if webhookURL == "" {
|
||||||
|
fmt.Fprintln(stderr, "Slack webhook URL is required. Set --webhook or SLACK_WEBHOOK_URL.")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
msg, err := message(fs.Args(), stdin)
|
msg, err := message(fs.Args(), stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(stderr, "read message error:", err)
|
fmt.Fprintln(stderr, "read message error:", err)
|
||||||
@ -52,8 +77,17 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Slack送信(ここはあとで internal/slack に逃がす)
|
client := slack.WebhookClient{
|
||||||
fmt.Fprintln(stdout, msg)
|
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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,3 +102,17 @@ func message(args []string, stdin io.Reader) (string, error) {
|
|||||||
return strings.TrimSpace(string(b)), nil
|
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]
|
echo "message" | slacksend [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
--title <title> Message title (shown as bold)
|
||||||
|
--webhook <url> Slack Incoming Webhook URL
|
||||||
-V, --version show version
|
-V, --version show version
|
||||||
-h, --help show this help
|
-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
|
||||||
|
}
|
||||||
@ -13,4 +13,4 @@ var (
|
|||||||
|
|
||||||
func String() string {
|
func String() string {
|
||||||
return Version + " (" + Commit + ", " + Date + ")"
|
return Version + " (" + Commit + ", " + Date + ")"
|
||||||
}
|
}
|
||||||
|
|||||||
15
scripts/build.sh
Normal file
15
scripts/build.sh
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
VERSION=$(git describe --tags --always --dirty)
|
||||||
|
COMMIT=$(git rev-parse --short HEAD)
|
||||||
|
DATE=$(date +"%Y-%m-%d")
|
||||||
|
|
||||||
|
echo "Building slacksend version: $VERSION, commit: $COMMIT, date: $DATE"
|
||||||
|
|
||||||
|
go build \
|
||||||
|
-ldflags "\
|
||||||
|
-X gitea.pglikers.com/tools/slacksend/internal/version.Version=$VERSION \
|
||||||
|
-X gitea.pglikers.com/tools/slacksend/internal/version.Commit=$COMMIT \
|
||||||
|
-X gitea.pglikers.com/tools/slacksend/internal/version.Date=$DATE" \
|
||||||
|
-o ./bin/slacksend \
|
||||||
|
./cmd/slacksend
|
||||||
Loading…
x
Reference in New Issue
Block a user