feat(whatsapp): enhance message handling and add media sending capabilities
This commit is contained in:
parent
eca076c094
commit
c823c3087a
1 changed files with 432 additions and 4 deletions
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
@ -33,6 +34,7 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/identity"
|
"github.com/sipeed/picoclaw/pkg/identity"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/media"
|
||||||
"github.com/sipeed/picoclaw/pkg/utils"
|
"github.com/sipeed/picoclaw/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -267,9 +269,17 @@ func (c *WhatsAppNativeChannel) Stop(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WhatsAppNativeChannel) eventHandler(evt any) {
|
func (c *WhatsAppNativeChannel) eventHandler(evt any) {
|
||||||
switch evt.(type) {
|
switch v := evt.(type) {
|
||||||
case *events.Message:
|
case *events.Message:
|
||||||
c.handleIncoming(evt.(*events.Message))
|
c.handleIncoming(v)
|
||||||
|
case *events.CallOffer:
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client != nil {
|
||||||
|
_ = client.RejectCall(context.Background(), v.BasicCallMeta.From, v.BasicCallMeta.CallID)
|
||||||
|
logger.InfoCF("whatsapp", "Call rejected automatically", map[string]any{"from": v.BasicCallMeta.From.String()})
|
||||||
|
}
|
||||||
case *events.Disconnected:
|
case *events.Disconnected:
|
||||||
logger.InfoCF("whatsapp", "WhatsApp disconnected, will attempt reconnection", nil)
|
logger.InfoCF("whatsapp", "WhatsApp disconnected, will attempt reconnection", nil)
|
||||||
c.reconnectMu.Lock()
|
c.reconnectMu.Lock()
|
||||||
|
|
@ -363,6 +373,23 @@ func (c *WhatsAppNativeChannel) handleIncoming(evt *events.Message) {
|
||||||
if evt.Info.PushName != "" {
|
if evt.Info.PushName != "" {
|
||||||
metadata["user_name"] = evt.Info.PushName
|
metadata["user_name"] = evt.Info.PushName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve LID to phone number if available
|
||||||
|
if evt.Info.Sender.Server == types.HiddenUserServer {
|
||||||
|
metadata["lid"] = evt.Info.Sender.User
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client != nil && client.Store.LIDs != nil {
|
||||||
|
if pnJID, err := client.Store.LIDs.GetPNForLID(c.runCtx, evt.Info.Sender); err == nil && !pnJID.IsEmpty() {
|
||||||
|
metadata["phone_number"] = "+" + pnJID.User
|
||||||
|
senderID = pnJID.String()
|
||||||
|
logger.DebugCF("whatsapp", "LID resolved", map[string]any{"lid": evt.Info.Sender.User, "phone": "+" + pnJID.User})
|
||||||
|
} else {
|
||||||
|
logger.WarnCF("whatsapp", "LID not found in DB — unknown sender", map[string]any{"lid": evt.Info.Sender.User})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if evt.Info.Chat.Server == types.GroupServer {
|
if evt.Info.Chat.Server == types.GroupServer {
|
||||||
metadata["peer_kind"] = "group"
|
metadata["peer_kind"] = "group"
|
||||||
metadata["peer_id"] = chatID
|
metadata["peer_id"] = chatID
|
||||||
|
|
@ -393,6 +420,19 @@ func (c *WhatsAppNativeChannel) handleIncoming(evt *events.Message) {
|
||||||
"WhatsApp message received",
|
"WhatsApp message received",
|
||||||
map[string]any{"sender_id": senderID, "content_preview": utils.Truncate(content, 50)},
|
map[string]any{"sender_id": senderID, "content_preview": utils.Truncate(content, 50)},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Send read receipt (blue ticks)
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client != nil {
|
||||||
|
senderJID := evt.Info.Sender
|
||||||
|
if evt.Info.Chat.Server != types.GroupServer {
|
||||||
|
senderJID = types.EmptyJID
|
||||||
|
}
|
||||||
|
_ = client.MarkRead(c.runCtx, []types.MessageID{evt.Info.ID}, time.Now(), evt.Info.Chat, senderJID)
|
||||||
|
}
|
||||||
|
|
||||||
c.HandleMessage(c.runCtx, peer, messageID, senderID, chatID, content, mediaPaths, metadata, sender)
|
c.HandleMessage(c.runCtx, peer, messageID, senderID, chatID, content, mediaPaths, metadata, sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,8 +465,16 @@ func (c *WhatsAppNativeChannel) Send(ctx context.Context, msg bus.OutboundMessag
|
||||||
return fmt.Errorf("invalid chat id %q: %w", msg.ChatID, err)
|
return fmt.Errorf("invalid chat id %q: %w", msg.ChatID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
waMsg := &waE2E.Message{
|
var waMsg *waE2E.Message
|
||||||
Conversation: proto.String(msg.Content),
|
if url := extractURL(msg.Content); url != "" {
|
||||||
|
waMsg = &waE2E.Message{
|
||||||
|
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
|
||||||
|
Text: proto.String(msg.Content),
|
||||||
|
MatchedText: proto.String(url),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
waMsg = &waE2E.Message{Conversation: proto.String(msg.Content)}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = client.SendMessage(ctx, to, waMsg); err != nil {
|
if _, err = client.SendMessage(ctx, to, waMsg); err != nil {
|
||||||
|
|
@ -435,6 +483,386 @@ func (c *WhatsAppNativeChannel) Send(ctx context.Context, msg bus.OutboundMessag
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMedia implements the channels.MediaSender interface.
|
||||||
|
func (c *WhatsAppNativeChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) error {
|
||||||
|
if !c.IsRunning() {
|
||||||
|
return channels.ErrNotRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if client == nil || !client.IsConnected() {
|
||||||
|
return fmt.Errorf("whatsapp connection not established: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
if client.Store.ID == nil {
|
||||||
|
return fmt.Errorf("whatsapp not yet paired (QR login pending): %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
|
||||||
|
to, err := parseJID(msg.ChatID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid chat id %q: %w", msg.ChatID, channels.ErrSendFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
store := c.GetMediaStore()
|
||||||
|
if store == nil {
|
||||||
|
return fmt.Errorf("no media store available: %w", channels.ErrSendFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, part := range msg.Parts {
|
||||||
|
if err := c.sendMediaPart(ctx, client, to, store, part); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendMediaPart uploads and sends a single media part via whatsmeow.
|
||||||
|
func (c *WhatsAppNativeChannel) sendMediaPart(
|
||||||
|
ctx context.Context,
|
||||||
|
client *whatsmeow.Client,
|
||||||
|
to types.JID,
|
||||||
|
store media.MediaStore,
|
||||||
|
part bus.MediaPart,
|
||||||
|
) error {
|
||||||
|
localPath, err := store.Resolve(part.Ref)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("whatsapp", "Failed to resolve media ref", map[string]any{
|
||||||
|
"ref": part.Ref,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("whatsapp resolve media ref %q: %w", part.Ref, channels.ErrSendFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(localPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("whatsapp", "Failed to read media file", map[string]any{
|
||||||
|
"path": localPath,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("whatsapp read media: %w", channels.ErrSendFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeType := part.ContentType
|
||||||
|
if mimeType == "" {
|
||||||
|
mimeType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
var waMsg *waE2E.Message
|
||||||
|
|
||||||
|
switch part.Type {
|
||||||
|
case "image":
|
||||||
|
resp, err := client.Upload(ctx, data, whatsmeow.MediaImage)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("whatsapp upload image: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
waMsg = &waE2E.Message{
|
||||||
|
ImageMessage: &waE2E.ImageMessage{
|
||||||
|
Caption: proto.String(part.Caption),
|
||||||
|
Mimetype: proto.String(mimeType),
|
||||||
|
URL: proto.String(resp.URL),
|
||||||
|
DirectPath: proto.String(resp.DirectPath),
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: proto.Uint64(resp.FileLength),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
case "video":
|
||||||
|
resp, err := client.Upload(ctx, data, whatsmeow.MediaVideo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("whatsapp upload video: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
waMsg = &waE2E.Message{
|
||||||
|
VideoMessage: &waE2E.VideoMessage{
|
||||||
|
Caption: proto.String(part.Caption),
|
||||||
|
Mimetype: proto.String(mimeType),
|
||||||
|
URL: proto.String(resp.URL),
|
||||||
|
DirectPath: proto.String(resp.DirectPath),
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: proto.Uint64(resp.FileLength),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
case "audio":
|
||||||
|
resp, err := client.Upload(ctx, data, whatsmeow.MediaAudio)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("whatsapp upload audio: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
// Detect voice messages (PTT) by filename convention
|
||||||
|
fn := strings.ToLower(part.Filename)
|
||||||
|
isPTT := strings.Contains(fn, "voice") || strings.Contains(fn, "ptt")
|
||||||
|
waMsg = &waE2E.Message{
|
||||||
|
AudioMessage: &waE2E.AudioMessage{
|
||||||
|
Mimetype: proto.String(mimeType),
|
||||||
|
PTT: proto.Bool(isPTT),
|
||||||
|
URL: proto.String(resp.URL),
|
||||||
|
DirectPath: proto.String(resp.DirectPath),
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: proto.Uint64(resp.FileLength),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
default: // "file" or unknown → send as document
|
||||||
|
resp, err := client.Upload(ctx, data, whatsmeow.MediaDocument)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("whatsapp upload document: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
filename := part.Filename
|
||||||
|
if filename == "" {
|
||||||
|
filename = filepath.Base(localPath)
|
||||||
|
}
|
||||||
|
waMsg = &waE2E.Message{
|
||||||
|
DocumentMessage: &waE2E.DocumentMessage{
|
||||||
|
Caption: proto.String(part.Caption),
|
||||||
|
Title: proto.String(filename),
|
||||||
|
FileName: proto.String(filename),
|
||||||
|
Mimetype: proto.String(mimeType),
|
||||||
|
URL: proto.String(resp.URL),
|
||||||
|
DirectPath: proto.String(resp.DirectPath),
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: proto.Uint64(resp.FileLength),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.SendMessage(ctx, to, waMsg); err != nil {
|
||||||
|
logger.ErrorCF("whatsapp", "Failed to send media", map[string]any{
|
||||||
|
"type": part.Type,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("whatsapp send media: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.DebugCF("whatsapp", "Media sent", map[string]any{
|
||||||
|
"type": part.Type,
|
||||||
|
"to": to.String(),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractURL returns the first URL found in text, or empty string.
|
||||||
|
var urlPattern = regexp.MustCompile(`https?://\S+`)
|
||||||
|
|
||||||
|
func extractURL(text string) string {
|
||||||
|
return urlPattern.FindString(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTyping sends a "composing" chat presence to the given chat.
|
||||||
|
// It repeats every 4 seconds (WhatsApp indicator expires after ~5s).
|
||||||
|
// The returned stop function sends a "paused" presence and is idempotent.
|
||||||
|
func (c *WhatsAppNativeChannel) StartTyping(ctx context.Context, chatID string) (func(), error) {
|
||||||
|
jid, err := parseJID(chatID)
|
||||||
|
if err != nil {
|
||||||
|
return func() {}, fmt.Errorf("start typing: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client == nil {
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = client.SendChatPresence(ctx, jid, types.ChatPresenceComposing, types.ChatPresenceMediaText)
|
||||||
|
|
||||||
|
typingCtx, typingCancel := context.WithCancel(ctx)
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(4 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-typingCtx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
c.mu.Lock()
|
||||||
|
cl := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if cl != nil {
|
||||||
|
_ = cl.SendChatPresence(typingCtx, jid, types.ChatPresenceComposing, types.ChatPresenceMediaText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
stop := func() {
|
||||||
|
once.Do(func() {
|
||||||
|
typingCancel()
|
||||||
|
c.mu.Lock()
|
||||||
|
cl := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if cl != nil {
|
||||||
|
_ = cl.SendChatPresence(context.Background(), jid, types.ChatPresencePaused, "")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return stop, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditMessage edits a previously sent message.
|
||||||
|
func (c *WhatsAppNativeChannel) EditMessage(ctx context.Context, chatID, messageID, content string) error {
|
||||||
|
jid, err := parseJID(chatID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("edit message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client == nil {
|
||||||
|
return fmt.Errorf("whatsapp not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
edited := client.BuildEdit(jid, types.MessageID(messageID), &waE2E.Message{
|
||||||
|
Conversation: proto.String(content),
|
||||||
|
})
|
||||||
|
_, err = client.SendMessage(ctx, jid, edited)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMessage revokes (deletes for everyone) a previously sent message.
|
||||||
|
func (c *WhatsAppNativeChannel) DeleteMessage(ctx context.Context, chatID, messageID string) error {
|
||||||
|
jid, err := parseJID(chatID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client == nil {
|
||||||
|
return fmt.Errorf("whatsapp not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
revoke := client.BuildRevoke(jid, types.EmptyJID, types.MessageID(messageID))
|
||||||
|
_, err = client.SendMessage(ctx, jid, revoke)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReactToMessage adds a 👀 reaction to an inbound message.
|
||||||
|
// The returned undo function removes the reaction (idempotent).
|
||||||
|
func (c *WhatsAppNativeChannel) ReactToMessage(ctx context.Context, chatID, messageID string) (func(), error) {
|
||||||
|
jid, err := parseJID(chatID)
|
||||||
|
if err != nil {
|
||||||
|
return func() {}, fmt.Errorf("react: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client == nil {
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reaction := client.BuildReaction(jid, types.EmptyJID, types.MessageID(messageID), "👀")
|
||||||
|
_, _ = client.SendMessage(ctx, jid, reaction)
|
||||||
|
|
||||||
|
var once sync.Once
|
||||||
|
undo := func() {
|
||||||
|
once.Do(func() {
|
||||||
|
c.mu.Lock()
|
||||||
|
cl := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if cl != nil {
|
||||||
|
unreact := cl.BuildReaction(jid, types.EmptyJID, types.MessageID(messageID), "")
|
||||||
|
_, _ = cl.SendMessage(context.Background(), jid, unreact)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return undo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendPlaceholder sends a temporary "thinking" message and returns its ID
|
||||||
|
// so it can be edited later with the real response.
|
||||||
|
func (c *WhatsAppNativeChannel) SendPlaceholder(ctx context.Context, chatID string) (string, error) {
|
||||||
|
jid, err := parseJID(chatID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("placeholder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client == nil {
|
||||||
|
return "", fmt.Errorf("whatsapp not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &waE2E.Message{Conversation: proto.String("Buscando... ⏳")}
|
||||||
|
resp, err := client.SendMessage(ctx, jid, msg)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return resp.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOnWhatsApp checks if a phone number is registered on WhatsApp.
|
||||||
|
func (c *WhatsAppNativeChannel) IsOnWhatsApp(ctx context.Context, phone string) (bool, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client == nil {
|
||||||
|
return false, fmt.Errorf("whatsapp not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
cleaned := "+" + strings.TrimLeft(phone, "+")
|
||||||
|
resp, err := client.IsOnWhatsApp(ctx, []string{cleaned})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if len(resp) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return resp[0].IsIn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNewsletter creates a WhatsApp Channel (newsletter) and returns its JID.
|
||||||
|
func (c *WhatsAppNativeChannel) CreateNewsletter(ctx context.Context, name, description string) (string, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client == nil {
|
||||||
|
return "", fmt.Errorf("whatsapp not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := client.CreateNewsletter(ctx, whatsmeow.CreateNewsletterParams{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return meta.ID.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendToNewsletter sends a message to a WhatsApp Channel (newsletter).
|
||||||
|
func (c *WhatsAppNativeChannel) SendToNewsletter(ctx context.Context, newsletterJID, content string) error {
|
||||||
|
jid, err := types.ParseJID(newsletterJID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid newsletter JID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if client == nil {
|
||||||
|
return fmt.Errorf("whatsapp not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &waE2E.Message{Conversation: proto.String(content)}
|
||||||
|
_, err = client.SendMessage(ctx, jid, msg)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// parseJID converts a chat ID (phone number or JID string) to types.JID.
|
// parseJID converts a chat ID (phone number or JID string) to types.JID.
|
||||||
func parseJID(s string) (types.JID, error) {
|
func parseJID(s string) (types.JID, error) {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue