gitea, notion webhook
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

141 lines
3.5 KiB

package slack
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"sync"
"time"
"git/palnet/slack-notifier/internal/notify"
)
const apiBase = "https://slack.com/api"
// Client is a minimal Slack Web API client: resolve users by email and DM them.
type Client struct {
token string
http *http.Client
mu sync.Mutex
cache map[string]string // email -> user id ("" = looked up, not found)
}
func New(token string) *Client {
return &Client{
token: token,
http: &http.Client{Timeout: 10 * time.Second},
cache: make(map[string]string),
}
}
func (c *Client) LookupUserIDByEmail(ctx context.Context, email string) (string, bool) {
if email == "" {
return "", false
}
c.mu.Lock()
if id, ok := c.cache[email]; ok {
c.mu.Unlock()
return id, id != ""
}
c.mu.Unlock()
endpoint := apiBase + "/users.lookupByEmail?" + url.Values{"email": {email}}.Encode()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
req.Header.Set("Authorization", "Bearer "+c.token)
var out struct {
OK bool `json:"ok"`
Error string `json:"error"`
User struct {
ID string `json:"id"`
} `json:"user"`
}
if err := c.do(req, &out); err != nil {
slog.Warn("slack lookup failed", "email", email, "err", err)
return "", false
}
id := ""
if out.OK {
id = out.User.ID
} else {
slog.Warn("slack user lookup failed", "email", email, "error", out.Error)
}
c.mu.Lock()
c.cache[email] = id
c.mu.Unlock()
return id, id != ""
}
func (c *Client) PostMessage(ctx context.Context, channel, text string, blocks []notify.Block) bool {
if err := c.post(ctx, channel, text, blocks); err != nil {
slog.Error("slack postMessage failed", "channel", channel, "err", err)
return false
}
return true
}
// PostAlert는 PostMessage와 같지만 실패해도 slog로 기록하지 않는다.
// 에러 알림 핸들러 전용 — slog.Error를 부르면 알림이 다시 트리거되어 무한루프가 된다.
func (c *Client) PostAlert(ctx context.Context, channel, text string, blocks []notify.Block) error {
return c.post(ctx, channel, text, blocks)
}
func (c *Client) post(ctx context.Context, channel, text string, blocks []notify.Block) error {
payload := map[string]any{"channel": channel}
if text != "" {
payload["text"] = text
}
if len(blocks) > 0 {
payload["blocks"] = blocks
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, apiBase+"/chat.postMessage", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Content-Type", "application/json; charset=utf-8")
var out struct {
OK bool `json:"ok"`
Error string `json:"error"`
}
if err := c.do(req, &out); err != nil {
return err
}
if !out.OK {
return fmt.Errorf("slack api error: %s", out.Error)
}
return nil
}
// SendDM resolves email -> Slack user and DMs them. 담당자를 해석하지 못하면
// 채널로 보내지 않고 로그만 남기고 skip한다(브로드캐스트는 별도 경로에서 처리).
func (c *Client) SendDM(ctx context.Context, email, text string, blocks []notify.Block) bool {
if id, ok := c.LookupUserIDByEmail(ctx, email); ok {
return c.PostMessage(ctx, id, text, blocks)
}
slog.Info("email unresolved — DM skipped (logged only)", "email", email)
return false
}
func (c *Client) do(req *http.Request, out any) error {
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(data, out)
}