feat(whatsapp_native): multimodal inbound/outbound media support
- handleIncoming: download Image/Video/Audio/Document/Sticker via whatsmeow, store in MediaStore, append Telegram-style annotations ([image: photo], etc.) - Captions merged into text; text-only messages work without a connected client - SendMedia implements channels.MediaSender (upload + Image/Video/Audio/Document) - Tests: imageFilenameFromMime, caption preserved when download fails, media-only dropped when download fails Made-with: Cursor
This commit is contained in:
parent
938437bfc9
commit
e33515faa0
2 changed files with 457 additions and 19 deletions
|
|
@ -4,13 +4,18 @@ package whatsapp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.mau.fi/whatsmeow"
|
||||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
|
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||||
"go.mau.fi/whatsmeow/types"
|
"go.mau.fi/whatsmeow/types"
|
||||||
"go.mau.fi/whatsmeow/types/events"
|
"go.mau.fi/whatsmeow/types/events"
|
||||||
|
waLog "go.mau.fi/whatsmeow/util/log"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
"github.com/sipeed/picoclaw/pkg/channels"
|
"github.com/sipeed/picoclaw/pkg/channels"
|
||||||
|
|
@ -59,3 +64,126 @@ func TestHandleIncoming_DoesNotConsumeGenericCommandsLocally(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImageFilenameFromMime(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
mime string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"image/png", "photo.png"},
|
||||||
|
{"image/jpeg", "photo.jpg"},
|
||||||
|
{"image/gif", "photo.gif"},
|
||||||
|
{"image/webp", "photo.webp"},
|
||||||
|
{"", "photo.jpg"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := imageFilenameFromMime(tt.mime); got != tt.want {
|
||||||
|
t.Errorf("imageFilenameFromMime(%q) = %q, want %q", tt.mime, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// whatsappTestClient returns a disconnected whatsmeow client (valid store) for exercising handleIncoming media paths.
|
||||||
|
func whatsappTestClient(t *testing.T) *whatsmeow.Client {
|
||||||
|
t.Helper()
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := sql.Open("sqlite", "file::memory:?cache=shared&_foreign_keys=on")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = db.Close() })
|
||||||
|
if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
container := sqlstore.NewWithDB(db, "sqlite", waLog.Noop)
|
||||||
|
if err := container.Upgrade(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
device, err := container.GetFirstDevice(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return whatsmeow.NewClient(device, waLog.Noop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleIncoming_ImageCaptionDownloadFailsStillForwardsCaption(t *testing.T) {
|
||||||
|
messageBus := bus.NewMessageBus()
|
||||||
|
ch := &WhatsAppNativeChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("whatsapp_native", config.WhatsAppConfig{}, messageBus, nil),
|
||||||
|
runCtx: context.Background(),
|
||||||
|
client: whatsappTestClient(t),
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := &events.Message{
|
||||||
|
Info: types.MessageInfo{
|
||||||
|
MessageSource: types.MessageSource{
|
||||||
|
Sender: types.NewJID("1001", types.DefaultUserServer),
|
||||||
|
Chat: types.NewJID("1001", types.DefaultUserServer),
|
||||||
|
},
|
||||||
|
ID: "mid-img",
|
||||||
|
PushName: "Bob",
|
||||||
|
},
|
||||||
|
Message: &waE2E.Message{
|
||||||
|
ImageMessage: &waE2E.ImageMessage{
|
||||||
|
Caption: proto.String("look at this"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.handleIncoming(evt)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatal("timeout waiting for inbound message")
|
||||||
|
case inbound, ok := <-messageBus.InboundChan():
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected inbound message")
|
||||||
|
}
|
||||||
|
if inbound.Content != "look at this" {
|
||||||
|
t.Fatalf("content=%q, want caption only when download fails", inbound.Content)
|
||||||
|
}
|
||||||
|
if len(inbound.Media) != 0 {
|
||||||
|
t.Fatalf("expected no media refs when download fails, got %v", inbound.Media)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleIncoming_ImageOnlyNoCaptionDownloadFails_NoInbound(t *testing.T) {
|
||||||
|
messageBus := bus.NewMessageBus()
|
||||||
|
ch := &WhatsAppNativeChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("whatsapp_native", config.WhatsAppConfig{}, messageBus, nil),
|
||||||
|
runCtx: context.Background(),
|
||||||
|
client: whatsappTestClient(t),
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := &events.Message{
|
||||||
|
Info: types.MessageInfo{
|
||||||
|
MessageSource: types.MessageSource{
|
||||||
|
Sender: types.NewJID("1001", types.DefaultUserServer),
|
||||||
|
Chat: types.NewJID("1001", types.DefaultUserServer),
|
||||||
|
},
|
||||||
|
ID: "mid-img2",
|
||||||
|
PushName: "Bob",
|
||||||
|
},
|
||||||
|
Message: &waE2E.Message{
|
||||||
|
ImageMessage: &waE2E.ImageMessage{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.handleIncoming(evt)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case inbound, ok := <-messageBus.InboundChan():
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("unexpected inbound when media-only and download fails: %+v", inbound)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
// expected: no message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mime"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -346,17 +348,119 @@ func (c *WhatsAppNativeChannel) handleIncoming(evt *events.Message) {
|
||||||
}
|
}
|
||||||
senderID := evt.Info.Sender.String()
|
senderID := evt.Info.Sender.String()
|
||||||
chatID := evt.Info.Chat.String()
|
chatID := evt.Info.Chat.String()
|
||||||
content := evt.Message.GetConversation()
|
messageID := evt.Info.ID
|
||||||
if content == "" && evt.Message.ExtendedTextMessage != nil {
|
|
||||||
content = evt.Message.ExtendedTextMessage.GetText()
|
|
||||||
}
|
|
||||||
content = utils.SanitizeMessageContent(content)
|
|
||||||
|
|
||||||
if content == "" {
|
sender := bus.SenderInfo{
|
||||||
|
Platform: "whatsapp",
|
||||||
|
PlatformID: senderID,
|
||||||
|
CanonicalID: identity.BuildCanonicalID("whatsapp", senderID),
|
||||||
|
DisplayName: evt.Info.PushName,
|
||||||
|
}
|
||||||
|
if !c.IsAllowedSender(sender) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaPaths []string
|
m := evt.Message
|
||||||
|
needsClient := m.GetImageMessage() != nil || m.GetVideoMessage() != nil ||
|
||||||
|
m.GetAudioMessage() != nil || m.GetDocumentMessage() != nil || m.GetStickerMessage() != nil
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
client := c.client
|
||||||
|
c.mu.Unlock()
|
||||||
|
if needsClient && client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := m.GetConversation()
|
||||||
|
if content == "" && m.ExtendedTextMessage != nil {
|
||||||
|
content = m.ExtendedTextMessage.GetText()
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaPaths := []string{}
|
||||||
|
scope := channels.BuildMediaScope("whatsapp_native", chatID, messageID)
|
||||||
|
|
||||||
|
storeMedia := func(localPath, filename string) string {
|
||||||
|
if store := c.GetMediaStore(); store != nil {
|
||||||
|
ref, err := store.Store(localPath, media.MediaMeta{
|
||||||
|
Filename: filename,
|
||||||
|
Source: "whatsapp_native",
|
||||||
|
}, scope)
|
||||||
|
if err == nil {
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
logger.WarnCF("whatsapp", "Failed to store WhatsApp media in MediaStore", map[string]any{
|
||||||
|
"path": localPath, "error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return localPath
|
||||||
|
}
|
||||||
|
|
||||||
|
appendCaption := func(cap string) {
|
||||||
|
cap = strings.TrimSpace(cap)
|
||||||
|
if cap == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if content != "" {
|
||||||
|
content += "\n"
|
||||||
|
}
|
||||||
|
content += cap
|
||||||
|
}
|
||||||
|
|
||||||
|
appendAnnotation := func(line string) {
|
||||||
|
if content != "" {
|
||||||
|
content += "\n"
|
||||||
|
}
|
||||||
|
content += line
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadAndStore := func(dm whatsmeow.DownloadableMessage, storeName, annotation string) {
|
||||||
|
localPath, err := c.downloadWhatsAppMediaToTemp(c.runCtx, client, dm)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("whatsapp", "Failed to download WhatsApp media", map[string]any{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mediaPaths = append(mediaPaths, storeMedia(localPath, storeName))
|
||||||
|
appendAnnotation(annotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if img := m.GetImageMessage(); img != nil {
|
||||||
|
appendCaption(img.GetCaption())
|
||||||
|
fname := imageFilenameFromMime(img.GetMimetype())
|
||||||
|
downloadAndStore(img, fname, "[image: photo]")
|
||||||
|
}
|
||||||
|
if vid := m.GetVideoMessage(); vid != nil {
|
||||||
|
appendCaption(vid.GetCaption())
|
||||||
|
downloadAndStore(vid, "video.mp4", "[video]")
|
||||||
|
}
|
||||||
|
if aud := m.GetAudioMessage(); aud != nil {
|
||||||
|
ann := "[audio]"
|
||||||
|
if aud.GetPTT() {
|
||||||
|
ann = "[voice]"
|
||||||
|
}
|
||||||
|
fname := "audio.ogg"
|
||||||
|
if mt := aud.GetMimetype(); strings.Contains(mt, "mpeg") || strings.Contains(mt, "mp3") {
|
||||||
|
fname = "audio.mp3"
|
||||||
|
}
|
||||||
|
downloadAndStore(aud, fname, ann)
|
||||||
|
}
|
||||||
|
if doc := m.GetDocumentMessage(); doc != nil {
|
||||||
|
appendCaption(doc.GetCaption())
|
||||||
|
fname := doc.GetFileName()
|
||||||
|
if strings.TrimSpace(fname) == "" {
|
||||||
|
fname = "document"
|
||||||
|
}
|
||||||
|
downloadAndStore(doc, fname, "[file: "+fname+"]")
|
||||||
|
}
|
||||||
|
if st := m.GetStickerMessage(); st != nil {
|
||||||
|
downloadAndStore(st, "sticker.webp", "[sticker]")
|
||||||
|
}
|
||||||
|
|
||||||
|
content = utils.SanitizeMessageContent(content)
|
||||||
|
if content == "" && len(mediaPaths) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
metadata := make(map[string]string)
|
metadata := make(map[string]string)
|
||||||
metadata["message_id"] = evt.Info.ID
|
metadata["message_id"] = evt.Info.ID
|
||||||
|
|
@ -376,26 +480,51 @@ func (c *WhatsAppNativeChannel) handleIncoming(evt *events.Message) {
|
||||||
peerKind = "group"
|
peerKind = "group"
|
||||||
}
|
}
|
||||||
peer := bus.Peer{Kind: peerKind, ID: chatID}
|
peer := bus.Peer{Kind: peerKind, ID: chatID}
|
||||||
messageID := evt.Info.ID
|
|
||||||
sender := bus.SenderInfo{
|
|
||||||
Platform: "whatsapp",
|
|
||||||
PlatformID: senderID,
|
|
||||||
CanonicalID: identity.BuildCanonicalID("whatsapp", senderID),
|
|
||||||
DisplayName: evt.Info.PushName,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.IsAllowedSender(sender) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.DebugCF(
|
logger.DebugCF(
|
||||||
"whatsapp",
|
"whatsapp",
|
||||||
"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), "media_count": len(mediaPaths)},
|
||||||
)
|
)
|
||||||
c.HandleMessage(c.runCtx, peer, messageID, senderID, chatID, content, mediaPaths, metadata, sender)
|
c.HandleMessage(c.runCtx, peer, messageID, senderID, chatID, content, mediaPaths, metadata, sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// downloadWhatsAppMediaToTemp decrypts and writes WhatsApp attachment bytes to a temp file.
|
||||||
|
func (c *WhatsAppNativeChannel) downloadWhatsAppMediaToTemp(ctx context.Context, cli *whatsmeow.Client, msg whatsmeow.DownloadableMessage) (path string, err error) {
|
||||||
|
data, err := cli.Download(ctx, msg)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
f, err := os.CreateTemp("", "whatsapp-native-media-*")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
path = f.Name()
|
||||||
|
if _, err = f.Write(data); err != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
_ = os.Remove(path)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err = f.Close(); err != nil {
|
||||||
|
_ = os.Remove(path)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageFilenameFromMime(mimetype string) string {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(mimetype, "png"):
|
||||||
|
return "photo.png"
|
||||||
|
case strings.Contains(mimetype, "gif"):
|
||||||
|
return "photo.gif"
|
||||||
|
case strings.Contains(mimetype, "webp"):
|
||||||
|
return "photo.webp"
|
||||||
|
default:
|
||||||
|
return "photo.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *WhatsAppNativeChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *WhatsAppNativeChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return channels.ErrNotRunning
|
||||||
|
|
@ -435,6 +564,187 @@ func (c *WhatsAppNativeChannel) Send(ctx context.Context, msg bus.OutboundMessag
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMedia implements channels.MediaSender for outbound images, audio, video, and files.
|
||||||
|
func (c *WhatsAppNativeChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) error {
|
||||||
|
if !c.IsRunning() {
|
||||||
|
return channels.ErrNotRunning
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
localPath, err := store.Resolve(part.Ref)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("whatsapp", "Failed to resolve media ref for send", map[string]any{
|
||||||
|
"ref": part.Ref, "error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("resolve media ref: %w", channels.ErrSendFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(localPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("whatsapp", "Failed to read media file for send", map[string]any{
|
||||||
|
"path": localPath, "error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("read media: %w", channels.ErrSendFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := whatsappOutboundMimetype(part, localPath)
|
||||||
|
waMsg, err := buildWhatsAppMediaMessage(ctx, client, part.Type, data, mt, part.Caption, part.Filename)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("whatsapp", "Failed to build/upload WhatsApp media message", map[string]any{
|
||||||
|
"type": part.Type, "error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("whatsapp media upload: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = client.SendMessage(ctx, to, waMsg); err != nil {
|
||||||
|
logger.ErrorCF("whatsapp", "Failed to send WhatsApp media", map[string]any{
|
||||||
|
"type": part.Type, "error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("whatsapp send media: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func whatsappOutboundMimetype(part bus.MediaPart, localPath string) string {
|
||||||
|
if part.ContentType != "" {
|
||||||
|
return part.ContentType
|
||||||
|
}
|
||||||
|
ext := filepath.Ext(localPath)
|
||||||
|
if ext != "" {
|
||||||
|
if m := mime.TypeByExtension(ext); m != "" {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch part.Type {
|
||||||
|
case "image":
|
||||||
|
return "image/jpeg"
|
||||||
|
case "audio":
|
||||||
|
return "audio/mpeg"
|
||||||
|
case "video":
|
||||||
|
return "video/mp4"
|
||||||
|
default:
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWhatsAppMediaMessage(
|
||||||
|
ctx context.Context,
|
||||||
|
client *whatsmeow.Client,
|
||||||
|
partType string,
|
||||||
|
data []byte,
|
||||||
|
mimetype, caption, filenameHint string,
|
||||||
|
) (*waE2E.Message, error) {
|
||||||
|
switch partType {
|
||||||
|
case "image":
|
||||||
|
resp, err := client.Upload(ctx, data, whatsmeow.MediaImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
img := &waE2E.ImageMessage{
|
||||||
|
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 caption != "" {
|
||||||
|
img.Caption = proto.String(caption)
|
||||||
|
}
|
||||||
|
return &waE2E.Message{ImageMessage: img}, nil
|
||||||
|
|
||||||
|
case "video":
|
||||||
|
resp, err := client.Upload(ctx, data, whatsmeow.MediaVideo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
vid := &waE2E.VideoMessage{
|
||||||
|
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 caption != "" {
|
||||||
|
vid.Caption = proto.String(caption)
|
||||||
|
}
|
||||||
|
return &waE2E.Message{VideoMessage: vid}, nil
|
||||||
|
|
||||||
|
case "audio":
|
||||||
|
resp, err := client.Upload(ctx, data, whatsmeow.MediaAudio)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ptt := strings.Contains(mimetype, "ogg") || strings.Contains(mimetype, "opus")
|
||||||
|
aud := &waE2E.AudioMessage{
|
||||||
|
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),
|
||||||
|
PTT: proto.Bool(ptt),
|
||||||
|
}
|
||||||
|
return &waE2E.Message{AudioMessage: aud}, nil
|
||||||
|
|
||||||
|
default: // "file" and unknown
|
||||||
|
resp, err := client.Upload(ctx, data, whatsmeow.MediaDocument)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fname := strings.TrimSpace(filenameHint)
|
||||||
|
if fname == "" {
|
||||||
|
fname = "file"
|
||||||
|
}
|
||||||
|
doc := &waE2E.DocumentMessage{
|
||||||
|
Mimetype: proto.String(mimetype),
|
||||||
|
FileName: proto.String(fname),
|
||||||
|
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 caption != "" {
|
||||||
|
doc.Caption = proto.String(caption)
|
||||||
|
}
|
||||||
|
return &waE2E.Message{DocumentMessage: doc}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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