219 lines
5.2 KiB
Go
219 lines
5.2 KiB
Go
//go:build amd64 || arm64 || riscv64 || mips64 || ppc64
|
|
|
|
package channels
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
lark "github.com/larksuite/oapi-sdk-go/v3"
|
|
larkdispatcher "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
|
|
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
|
|
larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
"github.com/sipeed/picoclaw/pkg/utils"
|
|
)
|
|
|
|
type FeishuChannel struct {
|
|
*BaseChannel
|
|
config config.FeishuConfig
|
|
client *lark.Client
|
|
wsClient *larkws.Client
|
|
|
|
mu sync.Mutex
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*FeishuChannel, error) {
|
|
base := NewBaseChannel("feishu", cfg, bus, cfg.AllowFrom)
|
|
|
|
return &FeishuChannel{
|
|
BaseChannel: base,
|
|
config: cfg,
|
|
client: lark.NewClient(cfg.AppID, cfg.AppSecret),
|
|
}, nil
|
|
}
|
|
|
|
func (c *FeishuChannel) Start(ctx context.Context) error {
|
|
if c.config.AppID == "" || c.config.AppSecret == "" {
|
|
return fmt.Errorf("feishu app_id or app_secret is empty")
|
|
}
|
|
|
|
dispatcher := larkdispatcher.NewEventDispatcher(c.config.VerificationToken, c.config.EncryptKey).
|
|
OnP2MessageReceiveV1(c.handleMessageReceive)
|
|
|
|
runCtx, cancel := context.WithCancel(ctx)
|
|
|
|
c.mu.Lock()
|
|
c.cancel = cancel
|
|
c.wsClient = larkws.NewClient(
|
|
c.config.AppID,
|
|
c.config.AppSecret,
|
|
larkws.WithEventHandler(dispatcher),
|
|
)
|
|
wsClient := c.wsClient
|
|
c.mu.Unlock()
|
|
|
|
c.setRunning(true)
|
|
logger.InfoC("feishu", "Feishu channel started (websocket mode)")
|
|
|
|
go func() {
|
|
if err := wsClient.Start(runCtx); err != nil {
|
|
logger.ErrorCF("feishu", "Feishu websocket stopped with error", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *FeishuChannel) Stop(ctx context.Context) error {
|
|
c.mu.Lock()
|
|
if c.cancel != nil {
|
|
c.cancel()
|
|
c.cancel = nil
|
|
}
|
|
c.wsClient = nil
|
|
c.mu.Unlock()
|
|
|
|
c.setRunning(false)
|
|
logger.InfoC("feishu", "Feishu channel stopped")
|
|
return nil
|
|
}
|
|
|
|
func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
|
if !c.IsRunning() {
|
|
return fmt.Errorf("feishu channel not running")
|
|
}
|
|
|
|
if msg.ChatID == "" {
|
|
return fmt.Errorf("chat ID is empty")
|
|
}
|
|
|
|
payload, err := json.Marshal(map[string]string{"text": msg.Content})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal feishu content: %w", err)
|
|
}
|
|
|
|
req := larkim.NewCreateMessageReqBuilder().
|
|
ReceiveIdType(larkim.ReceiveIdTypeChatId).
|
|
Body(larkim.NewCreateMessageReqBodyBuilder().
|
|
ReceiveId(msg.ChatID).
|
|
MsgType(larkim.MsgTypeText).
|
|
Content(string(payload)).
|
|
Uuid(fmt.Sprintf("picoclaw-%d", time.Now().UnixNano())).
|
|
Build()).
|
|
Build()
|
|
|
|
resp, err := c.client.Im.V1.Message.Create(ctx, req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send feishu message: %w", err)
|
|
}
|
|
|
|
if !resp.Success() {
|
|
return fmt.Errorf("feishu api error: code=%d msg=%s", resp.Code, resp.Msg)
|
|
}
|
|
|
|
logger.DebugCF("feishu", "Feishu message sent", map[string]interface{}{
|
|
"chat_id": msg.ChatID,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *FeishuChannel) handleMessageReceive(_ context.Context, event *larkim.P2MessageReceiveV1) error {
|
|
if event == nil || event.Event == nil || event.Event.Message == nil {
|
|
return nil
|
|
}
|
|
|
|
message := event.Event.Message
|
|
sender := event.Event.Sender
|
|
|
|
chatID := stringValue(message.ChatId)
|
|
if chatID == "" {
|
|
return nil
|
|
}
|
|
|
|
senderID := extractFeishuSenderID(sender)
|
|
if senderID == "" {
|
|
senderID = "unknown"
|
|
}
|
|
|
|
content := extractFeishuMessageContent(message)
|
|
if content == "" {
|
|
content = "[empty message]"
|
|
}
|
|
|
|
metadata := map[string]string{}
|
|
if messageID := stringValue(message.MessageId); messageID != "" {
|
|
metadata["message_id"] = messageID
|
|
}
|
|
if messageType := stringValue(message.MessageType); messageType != "" {
|
|
metadata["message_type"] = messageType
|
|
}
|
|
if chatType := stringValue(message.ChatType); chatType != "" {
|
|
metadata["chat_type"] = chatType
|
|
}
|
|
if sender != nil && sender.TenantKey != nil {
|
|
metadata["tenant_key"] = *sender.TenantKey
|
|
}
|
|
|
|
logger.InfoCF("feishu", "Feishu message received", map[string]interface{}{
|
|
"sender_id": senderID,
|
|
"chat_id": chatID,
|
|
"preview": utils.Truncate(content, 80),
|
|
})
|
|
|
|
c.HandleMessage(senderID, chatID, content, nil, metadata)
|
|
return nil
|
|
}
|
|
|
|
func extractFeishuSenderID(sender *larkim.EventSender) string {
|
|
if sender == nil || sender.SenderId == nil {
|
|
return ""
|
|
}
|
|
|
|
if sender.SenderId.UserId != nil && *sender.SenderId.UserId != "" {
|
|
return *sender.SenderId.UserId
|
|
}
|
|
if sender.SenderId.OpenId != nil && *sender.SenderId.OpenId != "" {
|
|
return *sender.SenderId.OpenId
|
|
}
|
|
if sender.SenderId.UnionId != nil && *sender.SenderId.UnionId != "" {
|
|
return *sender.SenderId.UnionId
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func extractFeishuMessageContent(message *larkim.EventMessage) string {
|
|
if message == nil || message.Content == nil || *message.Content == "" {
|
|
return ""
|
|
}
|
|
|
|
if message.MessageType != nil && *message.MessageType == larkim.MsgTypeText {
|
|
var textPayload struct {
|
|
Text string `json:"text"`
|
|
}
|
|
if err := json.Unmarshal([]byte(*message.Content), &textPayload); err == nil {
|
|
return textPayload.Text
|
|
}
|
|
}
|
|
|
|
return *message.Content
|
|
}
|
|
|
|
func stringValue(v *string) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
return *v
|
|
}
|