Major refactor to library
This commit is contained in:
678
pkg/whatsapp/whatsmeow/client.go
Normal file
678
pkg/whatsapp/whatsmeow/client.go
Normal file
@@ -0,0 +1,678 @@
|
||||
package whatsmeow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/events"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/logging"
|
||||
|
||||
qrterminal "github.com/mdp/qrterminal/v3"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
waEvents "go.mau.fi/whatsmeow/types/events"
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// Client represents a WhatsApp connection using whatsmeow
|
||||
type Client struct {
|
||||
id string
|
||||
phoneNumber string
|
||||
sessionPath string
|
||||
client *whatsmeow.Client
|
||||
container *sqlstore.Container
|
||||
eventBus *events.EventBus
|
||||
mediaConfig config.MediaConfig
|
||||
showQR bool
|
||||
keepAliveCancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewClient creates a new whatsmeow client
|
||||
func NewClient(cfg config.WhatsAppConfig, eventBus *events.EventBus, mediaConfig config.MediaConfig) (*Client, error) {
|
||||
if cfg.Type != "whatsmeow" && cfg.Type != "" {
|
||||
return nil, fmt.Errorf("invalid client type for whatsmeow: %s", cfg.Type)
|
||||
}
|
||||
|
||||
sessionPath := cfg.SessionPath
|
||||
if sessionPath == "" {
|
||||
sessionPath = fmt.Sprintf("./sessions/%s", cfg.ID)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
id: cfg.ID,
|
||||
phoneNumber: cfg.PhoneNumber,
|
||||
sessionPath: sessionPath,
|
||||
eventBus: eventBus,
|
||||
mediaConfig: mediaConfig,
|
||||
showQR: cfg.ShowQR,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Connect establishes a connection to WhatsApp
|
||||
func (c *Client) Connect(ctx context.Context) error {
|
||||
// Ensure session directory exists
|
||||
if err := os.MkdirAll(c.sessionPath, 0700); err != nil {
|
||||
return fmt.Errorf("failed to create session directory: %w", err)
|
||||
}
|
||||
|
||||
// Create database container for session storage
|
||||
dbPath := filepath.Join(c.sessionPath, "session.db")
|
||||
dbLog := waLog.Stdout("Database", "ERROR", true)
|
||||
container, err := sqlstore.New(ctx, "sqlite3", "file:"+dbPath+"?_foreign_keys=on", dbLog)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create database container: %w", err)
|
||||
}
|
||||
c.container = container
|
||||
|
||||
// Get device store
|
||||
deviceStore, err := container.GetFirstDevice(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get device: %w", err)
|
||||
}
|
||||
|
||||
// Set custom client information
|
||||
deviceStore.Platform = "WhatsHooked"
|
||||
deviceStore.BusinessName = "git.warky.dev/wdevs/whatshooked"
|
||||
|
||||
// Create client
|
||||
clientLog := waLog.Stdout("Client", "ERROR", true)
|
||||
client := whatsmeow.NewClient(deviceStore, clientLog)
|
||||
c.client = client
|
||||
|
||||
// Register event handler
|
||||
client.AddEventHandler(func(evt interface{}) {
|
||||
c.handleEvent(evt)
|
||||
})
|
||||
|
||||
// Connect
|
||||
if client.Store.ID == nil {
|
||||
// New device, need to pair
|
||||
qrChan, _ := client.GetQRChannel(ctx)
|
||||
if err := client.Connect(); err != nil {
|
||||
c.eventBus.Publish(events.WhatsAppPairFailedEvent(ctx, c.id, err))
|
||||
return fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
|
||||
// Wait for QR code
|
||||
for evt := range qrChan {
|
||||
switch evt.Event {
|
||||
case "code":
|
||||
logging.Info("QR code received for pairing", "account_id", c.id)
|
||||
|
||||
// Display QR code in terminal
|
||||
fmt.Println("\n========================================")
|
||||
fmt.Printf("WhatsApp QR Code for account: %s\n", c.id)
|
||||
fmt.Printf("Phone: %s\n", c.phoneNumber)
|
||||
fmt.Println("========================================")
|
||||
fmt.Println("Scan this QR code with WhatsApp on your phone:")
|
||||
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
|
||||
fmt.Println("========================================")
|
||||
|
||||
// Publish QR code event
|
||||
c.eventBus.Publish(events.WhatsAppQRCodeEvent(ctx, c.id, evt.Code))
|
||||
|
||||
case "success":
|
||||
logging.Info("Pairing successful", "account_id", c.id, "phone", c.phoneNumber)
|
||||
c.eventBus.Publish(events.WhatsAppPairSuccessEvent(ctx, c.id))
|
||||
|
||||
case "timeout":
|
||||
logging.Warn("QR code timeout", "account_id", c.id)
|
||||
c.eventBus.Publish(events.WhatsAppQRTimeoutEvent(ctx, c.id))
|
||||
|
||||
case "error":
|
||||
logging.Error("QR code error", "account_id", c.id, "error", evt.Error)
|
||||
c.eventBus.Publish(events.WhatsAppQRErrorEvent(ctx, c.id, fmt.Errorf("%v", evt.Error)))
|
||||
|
||||
default:
|
||||
logging.Info("Pairing event", "account_id", c.id, "event", evt.Event)
|
||||
c.eventBus.Publish(events.WhatsAppPairEventGeneric(ctx, c.id, evt.Event, map[string]any{
|
||||
"code": evt.Code,
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Already paired, just connect
|
||||
if err := client.Connect(); err != nil {
|
||||
return fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if deviceStore.PushName == "" {
|
||||
deviceStore.PushName = fmt.Sprintf("WhatsHooked %s", c.phoneNumber)
|
||||
if err := deviceStore.Save(ctx); err != nil {
|
||||
logging.Error("failed to save device store", "account_id", c.id)
|
||||
}
|
||||
}
|
||||
|
||||
if client.IsConnected() {
|
||||
err := client.SendPresence(ctx, types.PresenceAvailable)
|
||||
if err != nil {
|
||||
logging.Warn("Failed to send presence", "account_id", c.id, "error", err)
|
||||
} else {
|
||||
logging.Debug("Sent presence update", "account_id", c.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Start keep-alive routine
|
||||
c.startKeepAlive()
|
||||
|
||||
logging.Info("WhatsApp client connected", "account_id", c.id, "phone", c.phoneNumber)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect closes the WhatsApp connection
|
||||
func (c *Client) Disconnect() error {
|
||||
// Stop keep-alive
|
||||
if c.keepAliveCancel != nil {
|
||||
c.keepAliveCancel()
|
||||
}
|
||||
|
||||
if c.client != nil {
|
||||
c.client.Disconnect()
|
||||
}
|
||||
|
||||
logging.Info("WhatsApp client disconnected", "account_id", c.id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnected returns whether the client is connected
|
||||
func (c *Client) IsConnected() bool {
|
||||
if c.client == nil {
|
||||
return false
|
||||
}
|
||||
return c.client.IsConnected()
|
||||
}
|
||||
|
||||
// GetID returns the client ID
|
||||
func (c *Client) GetID() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
// GetPhoneNumber returns the phone number
|
||||
func (c *Client) GetPhoneNumber() string {
|
||||
return c.phoneNumber
|
||||
}
|
||||
|
||||
// GetType returns the client type
|
||||
func (c *Client) GetType() string {
|
||||
return "whatsmeow"
|
||||
}
|
||||
|
||||
// SendTextMessage sends a text message
|
||||
func (c *Client) SendTextMessage(ctx context.Context, jid types.JID, text string) (string, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if c.client == nil {
|
||||
err := fmt.Errorf("client not initialized")
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), text, err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
msg := &waE2E.Message{
|
||||
Conversation: proto.String(text),
|
||||
}
|
||||
|
||||
resp, err := c.client.SendMessage(ctx, jid, msg)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), text, err))
|
||||
return "", fmt.Errorf("failed to send message: %w", err)
|
||||
}
|
||||
|
||||
logging.Debug("Message sent", "account_id", c.id, "to", jid.String())
|
||||
c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, resp.ID, jid.String(), text))
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
// SendImage sends an image message
|
||||
func (c *Client) SendImage(ctx context.Context, jid types.JID, imageData []byte, mimeType string, caption string) (string, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if c.client == nil {
|
||||
err := fmt.Errorf("client not initialized")
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), caption, err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Upload the image
|
||||
uploaded, err := c.client.Upload(ctx, imageData, whatsmeow.MediaImage)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), caption, err))
|
||||
return "", fmt.Errorf("failed to upload image: %w", err)
|
||||
}
|
||||
|
||||
// Create image message
|
||||
msg := &waE2E.Message{
|
||||
ImageMessage: &waE2E.ImageMessage{
|
||||
URL: proto.String(uploaded.URL),
|
||||
DirectPath: proto.String(uploaded.DirectPath),
|
||||
MediaKey: uploaded.MediaKey,
|
||||
Mimetype: proto.String(mimeType),
|
||||
FileEncSHA256: uploaded.FileEncSHA256,
|
||||
FileSHA256: uploaded.FileSHA256,
|
||||
FileLength: proto.Uint64(uint64(len(imageData))),
|
||||
},
|
||||
}
|
||||
|
||||
// Add caption if provided
|
||||
if caption != "" {
|
||||
msg.ImageMessage.Caption = proto.String(caption)
|
||||
}
|
||||
|
||||
// Send the message
|
||||
resp, err := c.client.SendMessage(ctx, jid, msg)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), caption, err))
|
||||
return "", fmt.Errorf("failed to send image: %w", err)
|
||||
}
|
||||
|
||||
logging.Debug("Image sent", "account_id", c.id, "to", jid.String())
|
||||
c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, resp.ID, jid.String(), caption))
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
// SendVideo sends a video message
|
||||
func (c *Client) SendVideo(ctx context.Context, jid types.JID, videoData []byte, mimeType string, caption string) (string, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if c.client == nil {
|
||||
err := fmt.Errorf("client not initialized")
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), caption, err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Upload the video
|
||||
uploaded, err := c.client.Upload(ctx, videoData, whatsmeow.MediaVideo)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), caption, err))
|
||||
return "", fmt.Errorf("failed to upload video: %w", err)
|
||||
}
|
||||
|
||||
// Create video message
|
||||
msg := &waE2E.Message{
|
||||
VideoMessage: &waE2E.VideoMessage{
|
||||
URL: proto.String(uploaded.URL),
|
||||
DirectPath: proto.String(uploaded.DirectPath),
|
||||
MediaKey: uploaded.MediaKey,
|
||||
Mimetype: proto.String(mimeType),
|
||||
FileEncSHA256: uploaded.FileEncSHA256,
|
||||
FileSHA256: uploaded.FileSHA256,
|
||||
FileLength: proto.Uint64(uint64(len(videoData))),
|
||||
},
|
||||
}
|
||||
|
||||
// Add caption if provided
|
||||
if caption != "" {
|
||||
msg.VideoMessage.Caption = proto.String(caption)
|
||||
}
|
||||
|
||||
// Send the message
|
||||
resp, err := c.client.SendMessage(ctx, jid, msg)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), caption, err))
|
||||
return "", fmt.Errorf("failed to send video: %w", err)
|
||||
}
|
||||
|
||||
logging.Debug("Video sent", "account_id", c.id, "to", jid.String())
|
||||
c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, resp.ID, jid.String(), caption))
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
// SendDocument sends a document message
|
||||
func (c *Client) SendDocument(ctx context.Context, jid types.JID, documentData []byte, mimeType string, filename string, caption string) (string, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if c.client == nil {
|
||||
err := fmt.Errorf("client not initialized")
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), caption, err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Upload the document
|
||||
uploaded, err := c.client.Upload(ctx, documentData, whatsmeow.MediaDocument)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), caption, err))
|
||||
return "", fmt.Errorf("failed to upload document: %w", err)
|
||||
}
|
||||
|
||||
// Create document message
|
||||
msg := &waE2E.Message{
|
||||
DocumentMessage: &waE2E.DocumentMessage{
|
||||
URL: proto.String(uploaded.URL),
|
||||
DirectPath: proto.String(uploaded.DirectPath),
|
||||
MediaKey: uploaded.MediaKey,
|
||||
Mimetype: proto.String(mimeType),
|
||||
FileEncSHA256: uploaded.FileEncSHA256,
|
||||
FileSHA256: uploaded.FileSHA256,
|
||||
FileLength: proto.Uint64(uint64(len(documentData))),
|
||||
FileName: proto.String(filename),
|
||||
},
|
||||
}
|
||||
|
||||
// Add caption if provided
|
||||
if caption != "" {
|
||||
msg.DocumentMessage.Caption = proto.String(caption)
|
||||
}
|
||||
|
||||
// Send the message
|
||||
resp, err := c.client.SendMessage(ctx, jid, msg)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, jid.String(), caption, err))
|
||||
return "", fmt.Errorf("failed to send document: %w", err)
|
||||
}
|
||||
|
||||
logging.Debug("Document sent", "account_id", c.id, "to", jid.String(), "filename", filename)
|
||||
c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, resp.ID, jid.String(), caption))
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
// handleEvent processes WhatsApp events
|
||||
func (c *Client) handleEvent(evt interface{}) {
|
||||
ctx := context.Background()
|
||||
|
||||
switch v := evt.(type) {
|
||||
case *waEvents.Message:
|
||||
logging.Debug("Message received", "account_id", c.id, "from", v.Info.Sender.String())
|
||||
|
||||
// Extract message content based on type
|
||||
var text string
|
||||
var messageType string = "text"
|
||||
var mimeType string
|
||||
var filename string
|
||||
var mediaBase64 string
|
||||
var mediaURL string
|
||||
|
||||
// Handle text messages
|
||||
if v.Message.Conversation != nil {
|
||||
text = *v.Message.Conversation
|
||||
messageType = "text"
|
||||
} else if v.Message.ExtendedTextMessage != nil && v.Message.ExtendedTextMessage.Text != nil {
|
||||
text = *v.Message.ExtendedTextMessage.Text
|
||||
messageType = "text"
|
||||
}
|
||||
|
||||
// Handle image messages
|
||||
if v.Message.ImageMessage != nil {
|
||||
img := v.Message.ImageMessage
|
||||
messageType = "image"
|
||||
mimeType = img.GetMimetype()
|
||||
|
||||
if img.Caption != nil {
|
||||
text = *img.Caption
|
||||
}
|
||||
|
||||
// Download image
|
||||
data, err := c.client.Download(ctx, img)
|
||||
if err != nil {
|
||||
logging.Error("Failed to download image", "account_id", c.id, "error", err)
|
||||
} else {
|
||||
filename, mediaURL = c.processMediaData(v.Info.ID, data, mimeType, &mediaBase64)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle video messages
|
||||
if v.Message.VideoMessage != nil {
|
||||
vid := v.Message.VideoMessage
|
||||
messageType = "video"
|
||||
mimeType = vid.GetMimetype()
|
||||
|
||||
if vid.Caption != nil {
|
||||
text = *vid.Caption
|
||||
}
|
||||
|
||||
// Download video
|
||||
data, err := c.client.Download(ctx, vid)
|
||||
if err != nil {
|
||||
logging.Error("Failed to download video", "account_id", c.id, "error", err)
|
||||
} else {
|
||||
filename, mediaURL = c.processMediaData(v.Info.ID, data, mimeType, &mediaBase64)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle document messages
|
||||
if v.Message.DocumentMessage != nil {
|
||||
doc := v.Message.DocumentMessage
|
||||
messageType = "document"
|
||||
mimeType = doc.GetMimetype()
|
||||
|
||||
if doc.FileName != nil {
|
||||
filename = *doc.FileName
|
||||
}
|
||||
|
||||
if doc.Caption != nil {
|
||||
text = *doc.Caption
|
||||
}
|
||||
|
||||
// Download document
|
||||
data, err := c.client.Download(ctx, doc)
|
||||
if err != nil {
|
||||
logging.Error("Failed to download document", "account_id", c.id, "error", err)
|
||||
} else {
|
||||
filename, mediaURL = c.processMediaData(v.Info.ID, data, mimeType, &mediaBase64)
|
||||
}
|
||||
}
|
||||
|
||||
// Publish message received event
|
||||
c.eventBus.Publish(events.MessageReceivedEvent(
|
||||
ctx,
|
||||
c.id,
|
||||
v.Info.ID,
|
||||
v.Info.Sender.String(),
|
||||
v.Info.Chat.String(),
|
||||
text,
|
||||
v.Info.Timestamp,
|
||||
v.Info.IsGroup,
|
||||
"", // group name - TODO: extract from message
|
||||
"", // sender name - TODO: extract from message
|
||||
messageType,
|
||||
mimeType,
|
||||
filename,
|
||||
mediaBase64,
|
||||
mediaURL,
|
||||
))
|
||||
|
||||
case *waEvents.Connected:
|
||||
logging.Info("WhatsApp connected", "account_id", c.id)
|
||||
|
||||
// Get the actual phone number from WhatsApp
|
||||
phoneNumber := ""
|
||||
if c.client.Store.ID != nil {
|
||||
actualPhone := c.client.Store.ID.User
|
||||
phoneNumber = "+" + actualPhone
|
||||
|
||||
// Update phone number in client if it's different
|
||||
if c.phoneNumber != phoneNumber {
|
||||
c.phoneNumber = phoneNumber
|
||||
logging.Info("Updated phone number from WhatsApp", "account_id", c.id, "phone", phoneNumber)
|
||||
}
|
||||
} else if c.phoneNumber != "" {
|
||||
phoneNumber = c.phoneNumber
|
||||
}
|
||||
|
||||
c.eventBus.Publish(events.WhatsAppConnectedEvent(ctx, c.id, phoneNumber))
|
||||
|
||||
case *waEvents.Disconnected:
|
||||
logging.Warn("WhatsApp disconnected", "account_id", c.id)
|
||||
c.eventBus.Publish(events.WhatsAppDisconnectedEvent(ctx, c.id, "connection lost"))
|
||||
|
||||
case *waEvents.Receipt:
|
||||
// Handle delivery and read receipts
|
||||
if v.Type == types.ReceiptTypeDelivered {
|
||||
for _, messageID := range v.MessageIDs {
|
||||
logging.Debug("Message delivered", "account_id", c.id, "message_id", messageID, "from", v.Sender.String())
|
||||
c.eventBus.Publish(events.MessageDeliveredEvent(ctx, c.id, messageID, v.Sender.String(), v.Timestamp))
|
||||
}
|
||||
} else if v.Type == types.ReceiptTypeRead {
|
||||
for _, messageID := range v.MessageIDs {
|
||||
logging.Debug("Message read", "account_id", c.id, "message_id", messageID, "from", v.Sender.String())
|
||||
c.eventBus.Publish(events.MessageReadEvent(ctx, c.id, messageID, v.Sender.String(), v.Timestamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startKeepAlive starts a goroutine that sends presence updates to keep the connection alive
|
||||
func (c *Client) startKeepAlive() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c.keepAliveCancel = cancel
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(60 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logging.Debug("Keep-alive stopped", "account_id", c.id)
|
||||
return
|
||||
case <-ticker.C:
|
||||
if c.client != nil && c.client.IsConnected() {
|
||||
err := c.client.SendPresence(ctx, types.PresenceAvailable)
|
||||
if err != nil {
|
||||
logging.Warn("Failed to send presence", "account_id", c.id, "error", err)
|
||||
} else {
|
||||
logging.Debug("Sent presence update", "account_id", c.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
logging.Info("Keep-alive started", "account_id", c.id)
|
||||
}
|
||||
|
||||
// processMediaData processes media based on the configured mode
|
||||
func (c *Client) processMediaData(messageID string, data []byte, mimeType string, mediaBase64 *string) (string, string) {
|
||||
mode := c.mediaConfig.Mode
|
||||
var filename string
|
||||
var mediaURL string
|
||||
|
||||
// Generate filename
|
||||
ext := getExtensionFromMimeType(mimeType)
|
||||
hash := sha256.Sum256(data)
|
||||
hashStr := hex.EncodeToString(hash[:8])
|
||||
filename = fmt.Sprintf("%s_%s%s", messageID, hashStr, ext)
|
||||
|
||||
// Handle base64 mode
|
||||
if mode == "base64" || mode == "both" {
|
||||
*mediaBase64 = base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
// Handle link mode
|
||||
if mode == "link" || mode == "both" {
|
||||
// Save file to disk
|
||||
filePath, err := c.saveMediaFile(messageID, data, mimeType)
|
||||
if err != nil {
|
||||
logging.Error("Failed to save media file", "account_id", c.id, "message_id", messageID, "error", err)
|
||||
} else {
|
||||
// Extract just the filename from the full path
|
||||
filename = filepath.Base(filePath)
|
||||
mediaURL = c.generateMediaURL(messageID, filename)
|
||||
}
|
||||
}
|
||||
|
||||
return filename, mediaURL
|
||||
}
|
||||
|
||||
// saveMediaFile saves media data to disk and returns the file path
|
||||
func (c *Client) saveMediaFile(messageID string, data []byte, mimeType string) (string, error) {
|
||||
// Create account-specific media directory
|
||||
mediaDir := filepath.Join(c.mediaConfig.DataPath, c.id)
|
||||
if err := os.MkdirAll(mediaDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create media directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate unique filename using message ID and hash
|
||||
hash := sha256.Sum256(data)
|
||||
hashStr := hex.EncodeToString(hash[:8])
|
||||
ext := getExtensionFromMimeType(mimeType)
|
||||
filename := fmt.Sprintf("%s_%s%s", messageID, hashStr, ext)
|
||||
|
||||
// Full path to file
|
||||
filePath := filepath.Join(mediaDir, filename)
|
||||
|
||||
// Write file
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to write media file: %w", err)
|
||||
}
|
||||
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// generateMediaURL generates a URL for accessing stored media
|
||||
func (c *Client) generateMediaURL(messageID, filename string) string {
|
||||
baseURL := c.mediaConfig.BaseURL
|
||||
if baseURL == "" {
|
||||
baseURL = "http://localhost:8080"
|
||||
}
|
||||
return fmt.Sprintf("%s/api/media/%s/%s", baseURL, c.id, filename)
|
||||
}
|
||||
|
||||
// getExtensionFromMimeType returns the file extension for a given MIME type
|
||||
func getExtensionFromMimeType(mimeType string) string {
|
||||
extensions := map[string]string{
|
||||
// Images
|
||||
"image/jpeg": ".jpg",
|
||||
"image/jpg": ".jpg",
|
||||
"image/png": ".png",
|
||||
"image/gif": ".gif",
|
||||
"image/webp": ".webp",
|
||||
"image/bmp": ".bmp",
|
||||
"image/svg+xml": ".svg",
|
||||
|
||||
// Videos
|
||||
"video/mp4": ".mp4",
|
||||
"video/mpeg": ".mpeg",
|
||||
"video/quicktime": ".mov",
|
||||
"video/x-msvideo": ".avi",
|
||||
"video/webm": ".webm",
|
||||
"video/3gpp": ".3gp",
|
||||
|
||||
// Documents
|
||||
"application/pdf": ".pdf",
|
||||
"application/msword": ".doc",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
||||
"application/vnd.ms-excel": ".xls",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
||||
"application/vnd.ms-powerpoint": ".ppt",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
||||
"text/plain": ".txt",
|
||||
"text/html": ".html",
|
||||
"application/zip": ".zip",
|
||||
"application/x-rar-compressed": ".rar",
|
||||
"application/x-7z-compressed": ".7z",
|
||||
"application/json": ".json",
|
||||
"application/xml": ".xml",
|
||||
|
||||
// Audio
|
||||
"audio/mpeg": ".mp3",
|
||||
"audio/ogg": ".ogg",
|
||||
"audio/wav": ".wav",
|
||||
"audio/aac": ".aac",
|
||||
"audio/x-m4a": ".m4a",
|
||||
}
|
||||
|
||||
if ext, ok := extensions[mimeType]; ok {
|
||||
return ext
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user