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.
 
 
 
 
 

174 lines
3.9 KiB

// Package notion maps Notion webhook events to DMs for a page's assignees.
package notion
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"git/palnet/slack-notifier/internal/notify"
)
const (
apiBase = "https://api.notion.com/v1"
version = "2022-06-28"
)
// Client calls the Notion API to resolve a page's assignees into emails.
type Client struct {
token string
assigneeProperty string
http *http.Client
}
func New(token, assigneeProperty string) *Client {
return &Client{
token: token,
assigneeProperty: assigneeProperty,
http: &http.Client{Timeout: 10 * time.Second},
}
}
type event struct {
Type string `json:"type"`
Entity struct {
ID string `json:"id"`
Type string `json:"type"`
} `json:"entity"`
}
type property struct {
Type string `json:"type"`
Title []struct {
PlainText string `json:"plain_text"`
} `json:"title"`
People []struct {
ID string `json:"id"`
} `json:"people"`
}
type page struct {
URL string `json:"url"`
Properties map[string]property `json:"properties"`
}
// BuildNotifications parses a Notion event and returns DMs for the page assignees.
func (c *Client) BuildNotifications(ctx context.Context, body []byte) ([]notify.Notification, error) {
var ev event
if err := json.Unmarshal(body, &ev); err != nil {
return nil, err
}
if ev.Entity.Type != "page" || ev.Entity.ID == "" {
slog.Info("notion event entity is not a page — ignored", "type", ev.Entity.Type)
return nil, nil
}
var pg page
if err := c.get(ctx, "pages/"+ev.Entity.ID, &pg); err != nil {
slog.Warn("notion get page failed", "err", err)
return nil, nil
}
title := pageTitle(&pg)
body2 := title
if pg.URL != "" {
body2 = fmt.Sprintf("<%s|%s>", pg.URL, title)
}
evType := ev.Type
if evType == "" {
evType = "변경"
}
var notes []notify.Notification
for _, id := range c.assigneeIDs(&pg) {
email := c.resolveEmail(ctx, id)
if email == "" {
slog.Info("notion user has no accessible email — skipped", "user_id", id)
continue
}
notes = append(notes, notify.Notification{
Email: email,
Text: fmt.Sprintf("[Notion] 담당 페이지가 업데이트되었습니다 (%s)", evType),
Blocks: notify.SimpleBlocks(
"🗒 Notion 업데이트 — "+evType, body2, ""),
})
}
return notes, nil
}
func pageTitle(pg *page) string {
for _, prop := range pg.Properties {
if prop.Type == "title" {
s := ""
for _, t := range prop.Title {
s += t.PlainText
}
if s != "" {
return s
}
}
}
return "(제목 없음)"
}
func (c *Client) assigneeIDs(pg *page) []string {
prop, ok := pg.Properties[c.assigneeProperty]
if !ok || prop.Type != "people" {
slog.Info("notion page has no people property", "property", c.assigneeProperty)
return nil
}
ids := make([]string, 0, len(prop.People))
for _, p := range prop.People {
if p.ID != "" {
ids = append(ids, p.ID)
}
}
return ids
}
func (c *Client) resolveEmail(ctx context.Context, userID string) string {
var u struct {
Type string `json:"type"`
Person struct {
Email string `json:"email"`
} `json:"person"`
}
if err := c.get(ctx, "users/"+userID, &u); err != nil {
return ""
}
if u.Type != "person" {
return ""
}
return u.Person.Email
}
func (c *Client) get(ctx context.Context, path string, out any) error {
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, apiBase+"/"+path, nil)
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Notion-Version", version)
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
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("notion %s: status %d: %s", path, resp.StatusCode, truncate(data, 200))
}
return json.Unmarshal(data, out)
}
func truncate(b []byte, n int) string {
if len(b) > n {
return string(b[:n])
}
return string(b)
}