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.
 
 
 
 
 

138 lines
3.4 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 {
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 {
slog.Error("slack postMessage failed", "channel", channel, "err", err)
return false
}
if !out.OK {
slog.Error("slack postMessage failed", "channel", channel, "error", out.Error)
return false
}
return true
}
// SendDM resolves email -> Slack user and DMs them. Falls back to fallbackChannel
// if the email can't be resolved; if there's no fallback either, it is skipped.
func (c *Client) SendDM(ctx context.Context, email, text string, blocks []notify.Block, fallbackChannel string) bool {
if id, ok := c.LookupUserIDByEmail(ctx, email); ok {
return c.PostMessage(ctx, id, text, blocks)
}
if fallbackChannel != "" {
prefix := "(담당자 미지정) "
if email != "" {
prefix = fmt.Sprintf("(담당자 %s 매핑 실패) ", email)
}
slog.Info("email unresolved — routing to fallback channel", "email", email, "channel", fallbackChannel)
return c.PostMessage(ctx, fallbackChannel, prefix+text, blocks)
}
slog.Info("no Slack recipient and no fallback channel — skipped", "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)
}