package kook import ( "context" "fmt" "net/http" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) // REST endpoints for sending KOOK messages. const ( channelMessagePath = "/message/create" // channels (GROUP/BROADCAST) privateMessagePath = "/direct-message/create" // private chat ) // kookPostRequest is the shared body of the two KOOK send endpoints. type kookPostRequest struct { Type int `json:"type"` TargetID string `json:"target_id"` Content string `json:"content"` Quote string `json:"quote,omitempty"` } // SendMessage delivers the reply messages in resp. Each message goes to its own // channel (GROUP) or private conversation (PERSON/BROADCAST). func (a *Adapter) SendMessage(ctx context.Context, resp adapter.UnifiedResponse) error { if err := a.requireConnected(); err != nil { return err } for i, m := range resp.Messages { if err := a.sendOne(ctx, m, resp.ReplyTo); err != nil { return fmt.Errorf("kook: send message #%d: %w", i, err) } } return nil } // SendProactive pushes a message not triggered by a recent inbound. KOOK allows // arbitrary proactive push, so this is only gated by the connection state. func (a *Adapter) SendProactive(ctx context.Context, msg adapter.UnifiedMessage) error { if err := a.requireConnected(); err != nil { return err } return a.sendOne(ctx, msg, "") } // sendOne delivers a single outbound message to its conversation, choosing the // send endpoint that matches the channel kind and the message type that matches // the content kind. func (a *Adapter) sendOne(ctx context.Context, m adapter.UnifiedMessage, replyTo string) error { group := m.ChannelType == adapter.ChannelGroup target := m.ChannelID if group { if target == "" { target = m.GroupID } } else { if target == "" { target = m.SenderID } } if target == "" { return fmt.Errorf("kook: %w", ErrNoChatTarget) } msgType, content := outboundType(m) req := kookPostRequest{Type: msgType, TargetID: target, Content: content} if replyTo != "" { req.Quote = replyTo } path := channelMessagePath if !group { path = privateMessagePath } if _, err := a.http.DoJSON(ctx, http.MethodPost, path, req); err != nil { return err } a.log.Debug("sent", "type", msgType, "target", target, "group", group) return nil } // outboundType maps a unified message to a KOOK message type and content. Text // falls back to kmarkdown (type 9) because the adapter declares the Markdown // capability; media carries the first media URL in the content field. func outboundType(m adapter.UnifiedMessage) (int, string) { switch m.ContentType { case adapter.ContentImage: return kindImage, mediaURL(m.Media) case adapter.ContentVoice: return kindAudio, mediaURL(m.Media) case adapter.ContentFile: if mediaKind(m.Media) == adapter.MediaVideo { return kindVideo, mediaURL(m.Media) } return kindFile, mediaURL(m.Media) default: return kindKMarkdown, m.Content } } // mediaURL returns the first non-empty media URL. func mediaURL(media []adapter.Media) string { for _, m := range media { if m.URL != "" { return m.URL } } return "" } // mediaKind returns the kind of the first media entry. func mediaKind(media []adapter.Media) adapter.MediaKind { for _, m := range media { return m.Kind } return "" }