Browse Source
fix: null webhook message when edit (#221)
fix: null webhook message when edit (#221)
* fix: update deps & bug fix fix(go.mod): update module dependencies to latest versions to ensure compatibility and security fix(whatsapp.go): enhance event message handling by adding quoted message field and streamline message building and media extraction functions * fix: make webhook call async and add retry mechanism fix(init.go): wrap forwardToWebhook call in a goroutine to make it async feat(webhook.go): add timestamp field to the webhook payload fix(webhook.go): implement retry logic for submitWebhook with exponential backoff * feat: update version * feat: add max download size validation for media files - settings.go: add WhatsappSettingMaxDownloadSize for file size validation (500MB) - utils.go: check file size before writing media to disk, returning an error if it exceeds the max limit * fix: improve error handling and logging fix(utils.go): replace panic with logrus error logging and return false fix(webhook.go): add logrus error logging when media download fails fix(webhook.go): add logrus info and warning logging for webhook submission attempts * fix: add omitempty tags to JSON fields to avoid empty values refactor(webhook.go): restructure createPayload to check and add fields conditionally, improving clarity and preventing unnecessary fields refactor(webhook.go): handle media extraction only when media is present, improving readability and maintainability * fix: add nil check for device in whatsapp init fix(init.go): add check for nil device and log error before panic Ensure proper error handling when no device is foundpull/225/head v4.22.0
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 661 additions and 518 deletions
-
21src/config/settings.go
-
20src/go.mod
-
20src/go.sum
-
202src/pkg/whatsapp/init.go
-
252src/pkg/whatsapp/utils.go
-
166src/pkg/whatsapp/webhook.go
-
498src/pkg/whatsapp/whatsapp.go
@ -0,0 +1,202 @@ |
|||
package whatsapp |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"fmt" |
|||
"os" |
|||
"strings" |
|||
"sync/atomic" |
|||
"time" |
|||
|
|||
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" |
|||
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/websocket" |
|||
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" |
|||
"github.com/sirupsen/logrus" |
|||
"go.mau.fi/whatsmeow" |
|||
"go.mau.fi/whatsmeow/appstate" |
|||
"go.mau.fi/whatsmeow/proto/waE2E" |
|||
"go.mau.fi/whatsmeow/store" |
|||
"go.mau.fi/whatsmeow/store/sqlstore" |
|||
"go.mau.fi/whatsmeow/types" |
|||
"go.mau.fi/whatsmeow/types/events" |
|||
waLog "go.mau.fi/whatsmeow/util/log" |
|||
"google.golang.org/protobuf/proto" |
|||
) |
|||
|
|||
var ( |
|||
cli *whatsmeow.Client |
|||
log waLog.Logger |
|||
historySyncID int32 |
|||
startupTime = time.Now().Unix() |
|||
) |
|||
|
|||
type ExtractedMedia struct { |
|||
MediaPath string `json:"media_path"` |
|||
MimeType string `json:"mime_type"` |
|||
Caption string `json:"caption"` |
|||
} |
|||
|
|||
type evtReaction struct { |
|||
ID string `json:"id,omitempty"` |
|||
Message string `json:"message,omitempty"` |
|||
} |
|||
|
|||
type evtMessage struct { |
|||
ID string `json:"id,omitempty"` |
|||
Text string `json:"text,omitempty"` |
|||
RepliedId string `json:"replied_id,omitempty"` |
|||
QuotedMessage string `json:"quoted_message,omitempty"` |
|||
} |
|||
|
|||
func InitWaDB() *sqlstore.Container { |
|||
// Running Whatsapp
|
|||
log = waLog.Stdout("Main", config.WhatsappLogLevel, true) |
|||
dbLog := waLog.Stdout("Database", config.WhatsappLogLevel, true) |
|||
storeContainer, err := sqlstore.New("sqlite3", fmt.Sprintf("file:%s/%s?_foreign_keys=off", config.PathStorages, config.DBName), dbLog) |
|||
if err != nil { |
|||
log.Errorf("Failed to connect to database: %v", err) |
|||
panic(pkgError.InternalServerError(fmt.Sprintf("Failed to connect to database: %v", err))) |
|||
|
|||
} |
|||
return storeContainer |
|||
} |
|||
|
|||
func InitWaCLI(storeContainer *sqlstore.Container) *whatsmeow.Client { |
|||
device, err := storeContainer.GetFirstDevice() |
|||
if err != nil { |
|||
log.Errorf("Failed to get device: %v", err) |
|||
panic(err) |
|||
} |
|||
|
|||
if device == nil { |
|||
log.Errorf("No device found") |
|||
panic("No device found") |
|||
} |
|||
|
|||
osName := fmt.Sprintf("%s %s", config.AppOs, config.AppVersion) |
|||
store.DeviceProps.PlatformType = &config.AppPlatform |
|||
store.DeviceProps.Os = &osName |
|||
cli = whatsmeow.NewClient(device, waLog.Stdout("Client", config.WhatsappLogLevel, true)) |
|||
cli.EnableAutoReconnect = true |
|||
cli.AutoTrustIdentity = true |
|||
cli.AddEventHandler(handler) |
|||
|
|||
return cli |
|||
} |
|||
|
|||
func handler(rawEvt interface{}) { |
|||
switch evt := rawEvt.(type) { |
|||
case *events.DeleteForMe: |
|||
log.Infof("Deleted message %s for %s", evt.MessageID, evt.SenderJID.String()) |
|||
case *events.AppStateSyncComplete: |
|||
if len(cli.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock { |
|||
err := cli.SendPresence(types.PresenceAvailable) |
|||
if err != nil { |
|||
log.Warnf("Failed to send available presence: %v", err) |
|||
} else { |
|||
log.Infof("Marked self as available") |
|||
} |
|||
} |
|||
case *events.PairSuccess: |
|||
websocket.Broadcast <- websocket.BroadcastMessage{ |
|||
Code: "LOGIN_SUCCESS", |
|||
Message: fmt.Sprintf("Successfully pair with %s", evt.ID.String()), |
|||
} |
|||
case *events.LoggedOut: |
|||
websocket.Broadcast <- websocket.BroadcastMessage{ |
|||
Code: "LIST_DEVICES", |
|||
Result: nil, |
|||
} |
|||
case *events.Connected, *events.PushNameSetting: |
|||
if len(cli.Store.PushName) == 0 { |
|||
return |
|||
} |
|||
|
|||
// Send presence available when connecting and when the pushname is changed.
|
|||
// This makes sure that outgoing messages always have the right pushname.
|
|||
err := cli.SendPresence(types.PresenceAvailable) |
|||
if err != nil { |
|||
log.Warnf("Failed to send available presence: %v", err) |
|||
} else { |
|||
log.Infof("Marked self as available") |
|||
} |
|||
case *events.StreamReplaced: |
|||
os.Exit(0) |
|||
case *events.Message: |
|||
metaParts := []string{ |
|||
fmt.Sprintf("pushname: %s", evt.Info.PushName), |
|||
fmt.Sprintf("timestamp: %s", evt.Info.Timestamp), |
|||
} |
|||
if evt.Info.Type != "" { |
|||
metaParts = append(metaParts, fmt.Sprintf("type: %s", evt.Info.Type)) |
|||
} |
|||
if evt.Info.Category != "" { |
|||
metaParts = append(metaParts, fmt.Sprintf("category: %s", evt.Info.Category)) |
|||
} |
|||
if evt.IsViewOnce { |
|||
metaParts = append(metaParts, "view once") |
|||
} |
|||
|
|||
log.Infof("Received message %s from %s (%s): %+v", evt.Info.ID, evt.Info.SourceString(), strings.Join(metaParts, ", "), evt.Message) |
|||
|
|||
if img := evt.Message.GetImageMessage(); img != nil { |
|||
if path, err := ExtractMedia(config.PathStorages, img); err != nil { |
|||
log.Errorf("Failed to download image: %v", err) |
|||
} else { |
|||
log.Infof("Image downloaded to %s", path) |
|||
} |
|||
} |
|||
|
|||
if config.WhatsappAutoReplyMessage != "" && |
|||
!isGroupJid(evt.Info.Chat.String()) && |
|||
!strings.Contains(evt.Info.SourceString(), "broadcast") { |
|||
_, _ = cli.SendMessage(context.Background(), evt.Info.Sender, &waE2E.Message{Conversation: proto.String(config.WhatsappAutoReplyMessage)}) |
|||
} |
|||
|
|||
if config.WhatsappWebhook != "" && |
|||
!strings.Contains(evt.Info.SourceString(), "broadcast") && |
|||
!isFromMySelf(evt.Info.SourceString()) { |
|||
go func(evt *events.Message) { |
|||
if err := forwardToWebhook(evt); err != nil { |
|||
logrus.Error("Failed forward to webhook: ", err) |
|||
} |
|||
}(evt) |
|||
} |
|||
case *events.Receipt: |
|||
if evt.Type == types.ReceiptTypeRead || evt.Type == types.ReceiptTypeReadSelf { |
|||
log.Infof("%v was read by %s at %s", evt.MessageIDs, evt.SourceString(), evt.Timestamp) |
|||
} else if evt.Type == types.ReceiptTypeDelivered { |
|||
log.Infof("%s was delivered to %s at %s", evt.MessageIDs[0], evt.SourceString(), evt.Timestamp) |
|||
} |
|||
case *events.Presence: |
|||
if evt.Unavailable { |
|||
if evt.LastSeen.IsZero() { |
|||
log.Infof("%s is now offline", evt.From) |
|||
} else { |
|||
log.Infof("%s is now offline (last seen: %s)", evt.From, evt.LastSeen) |
|||
} |
|||
} else { |
|||
log.Infof("%s is now online", evt.From) |
|||
} |
|||
case *events.HistorySync: |
|||
id := atomic.AddInt32(&historySyncID, 1) |
|||
fileName := fmt.Sprintf("%s/history-%d-%s-%d-%s.json", config.PathStorages, startupTime, cli.Store.ID.String(), id, evt.Data.SyncType.String()) |
|||
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600) |
|||
if err != nil { |
|||
log.Errorf("Failed to open file to write history sync: %v", err) |
|||
return |
|||
} |
|||
enc := json.NewEncoder(file) |
|||
enc.SetIndent("", " ") |
|||
err = enc.Encode(evt.Data) |
|||
if err != nil { |
|||
log.Errorf("Failed to write history sync: %v", err) |
|||
return |
|||
} |
|||
log.Infof("Wrote history sync to %s", fileName) |
|||
_ = file.Close() |
|||
case *events.AppState: |
|||
log.Debugf("App state event: %+v / %+v", evt.Index, evt.SyncActionValue) |
|||
} |
|||
} |
|||
@ -0,0 +1,252 @@ |
|||
package whatsapp |
|||
|
|||
import ( |
|||
"crypto/hmac" |
|||
"crypto/sha256" |
|||
"encoding/hex" |
|||
"fmt" |
|||
"mime" |
|||
"os" |
|||
"regexp" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/google/uuid" |
|||
"github.com/sirupsen/logrus" |
|||
"go.mau.fi/whatsmeow/proto/waE2E" |
|||
|
|||
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" |
|||
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" |
|||
"go.mau.fi/whatsmeow" |
|||
"go.mau.fi/whatsmeow/types" |
|||
"go.mau.fi/whatsmeow/types/events" |
|||
) |
|||
|
|||
// ExtractMedia is a helper function to extract media from whatsapp
|
|||
func ExtractMedia(storageLocation string, mediaFile whatsmeow.DownloadableMessage) (extractedMedia ExtractedMedia, err error) { |
|||
if mediaFile == nil { |
|||
logrus.Info("Skip download because data is nil") |
|||
return extractedMedia, nil |
|||
} |
|||
|
|||
data, err := cli.Download(mediaFile) |
|||
if err != nil { |
|||
return extractedMedia, err |
|||
} |
|||
|
|||
// Validate file size before writing to disk
|
|||
maxFileSize := config.WhatsappSettingMaxDownloadSize |
|||
if int64(len(data)) > maxFileSize { |
|||
return extractedMedia, fmt.Errorf("file size exceeds the maximum limit of %d bytes", maxFileSize) |
|||
} |
|||
|
|||
switch media := mediaFile.(type) { |
|||
case *waE2E.ImageMessage: |
|||
extractedMedia.MimeType = media.GetMimetype() |
|||
extractedMedia.Caption = media.GetCaption() |
|||
case *waE2E.AudioMessage: |
|||
extractedMedia.MimeType = media.GetMimetype() |
|||
case *waE2E.VideoMessage: |
|||
extractedMedia.MimeType = media.GetMimetype() |
|||
extractedMedia.Caption = media.GetCaption() |
|||
case *waE2E.StickerMessage: |
|||
extractedMedia.MimeType = media.GetMimetype() |
|||
case *waE2E.DocumentMessage: |
|||
extractedMedia.MimeType = media.GetMimetype() |
|||
extractedMedia.Caption = media.GetCaption() |
|||
} |
|||
|
|||
var extension string |
|||
if ext, err := mime.ExtensionsByType(extractedMedia.MimeType); err == nil && len(ext) > 0 { |
|||
extension = ext[0] |
|||
} else if parts := strings.Split(extractedMedia.MimeType, "/"); len(parts) > 1 { |
|||
extension = "." + parts[len(parts)-1] |
|||
} |
|||
|
|||
extractedMedia.MediaPath = fmt.Sprintf("%s/%d-%s%s", storageLocation, time.Now().Unix(), uuid.NewString(), extension) |
|||
err = os.WriteFile(extractedMedia.MediaPath, data, 0600) |
|||
if err != nil { |
|||
return extractedMedia, err |
|||
} |
|||
return extractedMedia, nil |
|||
} |
|||
|
|||
func SanitizePhone(phone *string) { |
|||
if phone != nil && len(*phone) > 0 && !strings.Contains(*phone, "@") { |
|||
if len(*phone) <= 15 { |
|||
*phone = fmt.Sprintf("%s%s", *phone, config.WhatsappTypeUser) |
|||
} else { |
|||
*phone = fmt.Sprintf("%s%s", *phone, config.WhatsappTypeGroup) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func GetPlatformName(deviceID int) string { |
|||
switch deviceID { |
|||
case 0: |
|||
return "UNKNOWN" |
|||
case 1: |
|||
return "CHROME" |
|||
case 2: |
|||
return "FIREFOX" |
|||
case 3: |
|||
return "IE" |
|||
case 4: |
|||
return "OPERA" |
|||
case 5: |
|||
return "SAFARI" |
|||
case 6: |
|||
return "EDGE" |
|||
case 7: |
|||
return "DESKTOP" |
|||
case 8: |
|||
return "IPAD" |
|||
case 9: |
|||
return "ANDROID_TABLET" |
|||
case 10: |
|||
return "OHANA" |
|||
case 11: |
|||
return "ALOHA" |
|||
case 12: |
|||
return "CATALINA" |
|||
case 13: |
|||
return "TCL_TV" |
|||
default: |
|||
return "UNKNOWN" |
|||
} |
|||
} |
|||
|
|||
func ParseJID(arg string) (types.JID, error) { |
|||
if arg[0] == '+' { |
|||
arg = arg[1:] |
|||
} |
|||
if !strings.ContainsRune(arg, '@') { |
|||
return types.NewJID(arg, types.DefaultUserServer), nil |
|||
} |
|||
|
|||
recipient, err := types.ParseJID(arg) |
|||
if err != nil { |
|||
fmt.Printf("invalid JID %s: %v", arg, err) |
|||
return recipient, pkgError.ErrInvalidJID |
|||
} |
|||
|
|||
if recipient.User == "" { |
|||
fmt.Printf("invalid JID %v: no server specified", arg) |
|||
return recipient, pkgError.ErrInvalidJID |
|||
} |
|||
return recipient, nil |
|||
} |
|||
|
|||
func IsOnWhatsapp(waCli *whatsmeow.Client, jid string) bool { |
|||
// only check if the jid a user with @s.whatsapp.net
|
|||
if strings.Contains(jid, "@s.whatsapp.net") { |
|||
data, err := waCli.IsOnWhatsApp([]string{jid}) |
|||
if err != nil { |
|||
logrus.Error("Failed to check if user is on whatsapp: ", err) |
|||
return false |
|||
} |
|||
|
|||
for _, v := range data { |
|||
if !v.IsIn { |
|||
return false |
|||
} |
|||
} |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
func ValidateJidWithLogin(waCli *whatsmeow.Client, jid string) (types.JID, error) { |
|||
MustLogin(waCli) |
|||
|
|||
if config.WhatsappAccountValidation && !IsOnWhatsapp(waCli, jid) { |
|||
return types.JID{}, pkgError.InvalidJID(fmt.Sprintf("Phone %s is not on whatsapp", jid)) |
|||
} |
|||
|
|||
return ParseJID(jid) |
|||
} |
|||
|
|||
func MustLogin(waCli *whatsmeow.Client) { |
|||
if waCli == nil { |
|||
panic(pkgError.InternalServerError("Whatsapp client is not initialized")) |
|||
} |
|||
if !waCli.IsConnected() { |
|||
panic(pkgError.ErrNotConnected) |
|||
} else if !waCli.IsLoggedIn() { |
|||
panic(pkgError.ErrNotLoggedIn) |
|||
} |
|||
} |
|||
|
|||
// isGroupJid is a helper function to check if the message is from group
|
|||
func isGroupJid(jid string) bool { |
|||
return strings.Contains(jid, "@g.us") |
|||
} |
|||
|
|||
// isFromMySelf is a helper function to check if the message is from my self (logged in account)
|
|||
func isFromMySelf(jid string) bool { |
|||
return extractPhoneNumber(jid) == extractPhoneNumber(cli.Store.ID.String()) |
|||
} |
|||
|
|||
// extractPhoneNumber is a helper function to extract the phone number from a JID
|
|||
func extractPhoneNumber(jid string) string { |
|||
regex := regexp.MustCompile(`\d+`) |
|||
// Find all matches of the pattern in the JID
|
|||
matches := regex.FindAllString(jid, -1) |
|||
// The first match should be the phone number
|
|||
if len(matches) > 0 { |
|||
return matches[0] |
|||
} |
|||
// If no matches are found, return an empty string
|
|||
return "" |
|||
} |
|||
|
|||
func getMessageDigestOrSignature(msg, key []byte) (string, error) { |
|||
mac := hmac.New(sha256.New, key) |
|||
_, err := mac.Write(msg) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
return hex.EncodeToString(mac.Sum(nil)), nil |
|||
} |
|||
|
|||
func buildEventMessage(evt *events.Message) (message evtMessage) { |
|||
message.Text = evt.Message.GetConversation() |
|||
message.ID = evt.Info.ID |
|||
|
|||
if extendedMessage := evt.Message.GetExtendedTextMessage(); extendedMessage != nil { |
|||
message.Text = extendedMessage.GetText() |
|||
message.RepliedId = extendedMessage.ContextInfo.GetStanzaID() |
|||
message.QuotedMessage = extendedMessage.ContextInfo.GetQuotedMessage().GetConversation() |
|||
} else if protocolMessage := evt.Message.GetProtocolMessage(); protocolMessage != nil { |
|||
if editedMessage := protocolMessage.GetEditedMessage(); editedMessage != nil { |
|||
if extendedText := editedMessage.GetExtendedTextMessage(); extendedText != nil { |
|||
message.Text = extendedText.GetText() |
|||
message.RepliedId = extendedText.ContextInfo.GetStanzaID() |
|||
message.QuotedMessage = extendedText.ContextInfo.GetQuotedMessage().GetConversation() |
|||
} |
|||
} |
|||
} |
|||
|
|||
return message |
|||
} |
|||
|
|||
func buildEventReaction(evt *events.Message) (waReaction evtReaction) { |
|||
if reactionMessage := evt.Message.GetReactionMessage(); reactionMessage != nil { |
|||
waReaction.Message = reactionMessage.GetText() |
|||
waReaction.ID = reactionMessage.GetKey().GetID() |
|||
} |
|||
return waReaction |
|||
} |
|||
|
|||
func buildForwarded(evt *events.Message) bool { |
|||
if extendedText := evt.Message.GetExtendedTextMessage(); extendedText != nil { |
|||
return extendedText.ContextInfo.GetIsForwarded() |
|||
} else if protocolMessage := evt.Message.GetProtocolMessage(); protocolMessage != nil { |
|||
if editedMessage := protocolMessage.GetEditedMessage(); editedMessage != nil { |
|||
if extendedText := editedMessage.GetExtendedTextMessage(); extendedText != nil { |
|||
return extendedText.ContextInfo.GetIsForwarded() |
|||
} |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
@ -0,0 +1,166 @@ |
|||
package whatsapp |
|||
|
|||
import ( |
|||
"bytes" |
|||
"encoding/json" |
|||
"fmt" |
|||
"net/http" |
|||
"time" |
|||
|
|||
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" |
|||
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" |
|||
"github.com/sirupsen/logrus" |
|||
"go.mau.fi/whatsmeow/types/events" |
|||
) |
|||
|
|||
// forwardToWebhook is a helper function to forward event to webhook url
|
|||
func forwardToWebhook(evt *events.Message) error { |
|||
logrus.Info("Forwarding event to webhook:", config.WhatsappWebhook) |
|||
payload, err := createPayload(evt) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
if err = submitWebhook(payload); err != nil { |
|||
return err |
|||
} |
|||
|
|||
logrus.Info("Event forwarded to webhook") |
|||
return nil |
|||
} |
|||
|
|||
func createPayload(evt *events.Message) (map[string]interface{}, error) { |
|||
message := buildEventMessage(evt) |
|||
waReaction := buildEventReaction(evt) |
|||
forwarded := buildForwarded(evt) |
|||
|
|||
body := make(map[string]interface{}) |
|||
|
|||
if from := evt.Info.SourceString(); from != "" { |
|||
body["from"] = from |
|||
} |
|||
if message.Text != "" { |
|||
body["message"] = message |
|||
} |
|||
if pushname := evt.Info.PushName; pushname != "" { |
|||
body["pushname"] = pushname |
|||
} |
|||
if waReaction.Message != "" { |
|||
body["reaction"] = waReaction |
|||
} |
|||
if evt.IsViewOnce { |
|||
body["view_once"] = evt.IsViewOnce |
|||
} |
|||
if forwarded { |
|||
body["forwarded"] = forwarded |
|||
} |
|||
if timestamp := evt.Info.Timestamp.Format(time.RFC3339); timestamp != "" { |
|||
body["timestamp"] = timestamp |
|||
} |
|||
|
|||
if audioMedia := evt.Message.GetAudioMessage(); audioMedia != nil { |
|||
path, err := ExtractMedia(config.PathMedia, audioMedia) |
|||
if err != nil { |
|||
logrus.Errorf("Failed to download audio from %s: %v", evt.Info.SourceString(), err) |
|||
return nil, pkgError.WebhookError(fmt.Sprintf("Failed to download audio: %v", err)) |
|||
} |
|||
body["audio"] = path |
|||
} |
|||
|
|||
if contactMessage := evt.Message.GetContactMessage(); contactMessage != nil { |
|||
body["contact"] = contactMessage |
|||
} |
|||
|
|||
if documentMedia := evt.Message.GetDocumentMessage(); documentMedia != nil { |
|||
path, err := ExtractMedia(config.PathMedia, documentMedia) |
|||
if err != nil { |
|||
logrus.Errorf("Failed to download document from %s: %v", evt.Info.SourceString(), err) |
|||
return nil, pkgError.WebhookError(fmt.Sprintf("Failed to download document: %v", err)) |
|||
} |
|||
body["document"] = path |
|||
} |
|||
|
|||
if imageMedia := evt.Message.GetImageMessage(); imageMedia != nil { |
|||
path, err := ExtractMedia(config.PathMedia, imageMedia) |
|||
if err != nil { |
|||
logrus.Errorf("Failed to download image from %s: %v", evt.Info.SourceString(), err) |
|||
return nil, pkgError.WebhookError(fmt.Sprintf("Failed to download image: %v", err)) |
|||
} |
|||
body["image"] = path |
|||
} |
|||
|
|||
if listMessage := evt.Message.GetListMessage(); listMessage != nil { |
|||
body["list"] = listMessage |
|||
} |
|||
|
|||
if liveLocationMessage := evt.Message.GetLiveLocationMessage(); liveLocationMessage != nil { |
|||
body["live_location"] = liveLocationMessage |
|||
} |
|||
|
|||
if locationMessage := evt.Message.GetLocationMessage(); locationMessage != nil { |
|||
body["location"] = locationMessage |
|||
} |
|||
|
|||
if orderMessage := evt.Message.GetOrderMessage(); orderMessage != nil { |
|||
body["order"] = orderMessage |
|||
} |
|||
|
|||
if stickerMedia := evt.Message.GetStickerMessage(); stickerMedia != nil { |
|||
path, err := ExtractMedia(config.PathMedia, stickerMedia) |
|||
if err != nil { |
|||
logrus.Errorf("Failed to download sticker from %s: %v", evt.Info.SourceString(), err) |
|||
return nil, pkgError.WebhookError(fmt.Sprintf("Failed to download sticker: %v", err)) |
|||
} |
|||
body["sticker"] = path |
|||
} |
|||
|
|||
if videoMedia := evt.Message.GetVideoMessage(); videoMedia != nil { |
|||
path, err := ExtractMedia(config.PathMedia, videoMedia) |
|||
if err != nil { |
|||
logrus.Errorf("Failed to download video from %s: %v", evt.Info.SourceString(), err) |
|||
return nil, pkgError.WebhookError(fmt.Sprintf("Failed to download video: %v", err)) |
|||
} |
|||
body["video"] = path |
|||
} |
|||
|
|||
return body, nil |
|||
} |
|||
|
|||
func submitWebhook(payload map[string]interface{}) error { |
|||
client := &http.Client{Timeout: 10 * time.Second} |
|||
|
|||
postBody, err := json.Marshal(payload) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("Failed to marshal body: %v", err)) |
|||
} |
|||
|
|||
req, err := http.NewRequest(http.MethodPost, config.WhatsappWebhook, bytes.NewBuffer(postBody)) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("error when create http object %v", err)) |
|||
} |
|||
|
|||
secretKey := []byte(config.WhatsappWebhookSecret) |
|||
signature, err := getMessageDigestOrSignature(postBody, secretKey) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("error when create signature %v", err)) |
|||
} |
|||
|
|||
req.Header.Set("Content-Type", "application/json") |
|||
req.Header.Set("X-Hub-Signature-256", fmt.Sprintf("sha256=%s", signature)) |
|||
|
|||
var attempt int |
|||
var maxAttempts = 5 |
|||
var sleepDuration = 1 * time.Second |
|||
|
|||
for attempt = 0; attempt < maxAttempts; attempt++ { |
|||
if _, err = client.Do(req); err == nil { |
|||
logrus.Infof("Successfully submitted webhook on attempt %d", attempt+1) |
|||
return nil |
|||
} |
|||
logrus.Warnf("Attempt %d to submit webhook failed: %v", attempt+1, err) |
|||
time.Sleep(sleepDuration) |
|||
sleepDuration *= 2 |
|||
} |
|||
|
|||
return pkgError.WebhookError(fmt.Sprintf("error when submit webhook after %d attempts: %v", attempt, err)) |
|||
} |
|||
@ -1,498 +0,0 @@ |
|||
package whatsapp |
|||
|
|||
import ( |
|||
"bytes" |
|||
"context" |
|||
"crypto/hmac" |
|||
"crypto/sha256" |
|||
"encoding/hex" |
|||
"encoding/json" |
|||
"fmt" |
|||
"mime" |
|||
"net/http" |
|||
"os" |
|||
"regexp" |
|||
"strings" |
|||
"sync/atomic" |
|||
"time" |
|||
|
|||
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" |
|||
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/websocket" |
|||
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" |
|||
"github.com/google/uuid" |
|||
"github.com/sirupsen/logrus" |
|||
"go.mau.fi/whatsmeow" |
|||
"go.mau.fi/whatsmeow/appstate" |
|||
"go.mau.fi/whatsmeow/proto/waE2E" |
|||
"go.mau.fi/whatsmeow/store" |
|||
"go.mau.fi/whatsmeow/store/sqlstore" |
|||
"go.mau.fi/whatsmeow/types" |
|||
"go.mau.fi/whatsmeow/types/events" |
|||
waLog "go.mau.fi/whatsmeow/util/log" |
|||
"google.golang.org/protobuf/proto" |
|||
) |
|||
|
|||
var ( |
|||
cli *whatsmeow.Client |
|||
log waLog.Logger |
|||
historySyncID int32 |
|||
startupTime = time.Now().Unix() |
|||
) |
|||
|
|||
type ExtractedMedia struct { |
|||
MediaPath string `json:"media_path"` |
|||
MimeType string `json:"mime_type"` |
|||
Caption string `json:"caption"` |
|||
} |
|||
|
|||
type evtReaction struct { |
|||
ID string `json:"id"` |
|||
Message string `json:"message"` |
|||
} |
|||
|
|||
type evtMessage struct { |
|||
ID string `json:"id"` |
|||
Text string `json:"text"` |
|||
RepliedId string `json:"replied_id"` |
|||
} |
|||
|
|||
func SanitizePhone(phone *string) { |
|||
if phone != nil && len(*phone) > 0 && !strings.Contains(*phone, "@") { |
|||
if len(*phone) <= 15 { |
|||
*phone = fmt.Sprintf("%s%s", *phone, config.WhatsappTypeUser) |
|||
} else { |
|||
*phone = fmt.Sprintf("%s%s", *phone, config.WhatsappTypeGroup) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func GetPlatformName(deviceID int) string { |
|||
switch deviceID { |
|||
case 0: |
|||
return "UNKNOWN" |
|||
case 1: |
|||
return "CHROME" |
|||
case 2: |
|||
return "FIREFOX" |
|||
case 3: |
|||
return "IE" |
|||
case 4: |
|||
return "OPERA" |
|||
case 5: |
|||
return "SAFARI" |
|||
case 6: |
|||
return "EDGE" |
|||
case 7: |
|||
return "DESKTOP" |
|||
case 8: |
|||
return "IPAD" |
|||
case 9: |
|||
return "ANDROID_TABLET" |
|||
case 10: |
|||
return "OHANA" |
|||
case 11: |
|||
return "ALOHA" |
|||
case 12: |
|||
return "CATALINA" |
|||
case 13: |
|||
return "TCL_TV" |
|||
default: |
|||
return "UNKNOWN" |
|||
} |
|||
} |
|||
|
|||
func ParseJID(arg string) (types.JID, error) { |
|||
if arg[0] == '+' { |
|||
arg = arg[1:] |
|||
} |
|||
if !strings.ContainsRune(arg, '@') { |
|||
return types.NewJID(arg, types.DefaultUserServer), nil |
|||
} |
|||
|
|||
recipient, err := types.ParseJID(arg) |
|||
if err != nil { |
|||
fmt.Printf("invalid JID %s: %v", arg, err) |
|||
return recipient, pkgError.ErrInvalidJID |
|||
} |
|||
|
|||
if recipient.User == "" { |
|||
fmt.Printf("invalid JID %v: no server specified", arg) |
|||
return recipient, pkgError.ErrInvalidJID |
|||
} |
|||
return recipient, nil |
|||
} |
|||
|
|||
func IsOnWhatsapp(waCli *whatsmeow.Client, jid string) bool { |
|||
// only check if the jid a user with @s.whatsapp.net
|
|||
if strings.Contains(jid, "@s.whatsapp.net") { |
|||
data, err := waCli.IsOnWhatsApp([]string{jid}) |
|||
if err != nil { |
|||
panic(pkgError.InvalidJID(err.Error())) |
|||
} |
|||
|
|||
for _, v := range data { |
|||
if !v.IsIn { |
|||
return false |
|||
} |
|||
} |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
func ValidateJidWithLogin(waCli *whatsmeow.Client, jid string) (types.JID, error) { |
|||
MustLogin(waCli) |
|||
|
|||
if config.WhatsappAccountValidation && !IsOnWhatsapp(waCli, jid) { |
|||
return types.JID{}, pkgError.InvalidJID(fmt.Sprintf("Phone %s is not on whatsapp", jid)) |
|||
} |
|||
|
|||
return ParseJID(jid) |
|||
} |
|||
|
|||
func InitWaDB() *sqlstore.Container { |
|||
// Running Whatsapp
|
|||
log = waLog.Stdout("Main", config.WhatsappLogLevel, true) |
|||
dbLog := waLog.Stdout("Database", config.WhatsappLogLevel, true) |
|||
storeContainer, err := sqlstore.New("sqlite3", fmt.Sprintf("file:%s/%s?_foreign_keys=off", config.PathStorages, config.DBName), dbLog) |
|||
if err != nil { |
|||
log.Errorf("Failed to connect to database: %v", err) |
|||
panic(pkgError.InternalServerError(fmt.Sprintf("Failed to connect to database: %v", err))) |
|||
|
|||
} |
|||
return storeContainer |
|||
} |
|||
|
|||
func InitWaCLI(storeContainer *sqlstore.Container) *whatsmeow.Client { |
|||
device, err := storeContainer.GetFirstDevice() |
|||
if err != nil { |
|||
log.Errorf("Failed to get device: %v", err) |
|||
panic(err) |
|||
} |
|||
|
|||
osName := fmt.Sprintf("%s %s", config.AppOs, config.AppVersion) |
|||
store.DeviceProps.PlatformType = &config.AppPlatform |
|||
store.DeviceProps.Os = &osName |
|||
cli = whatsmeow.NewClient(device, waLog.Stdout("Client", config.WhatsappLogLevel, true)) |
|||
cli.EnableAutoReconnect = true |
|||
cli.AutoTrustIdentity = true |
|||
cli.AddEventHandler(handler) |
|||
|
|||
return cli |
|||
} |
|||
|
|||
func MustLogin(waCli *whatsmeow.Client) { |
|||
if waCli == nil { |
|||
panic(pkgError.InternalServerError("Whatsapp client is not initialized")) |
|||
} |
|||
if !waCli.IsConnected() { |
|||
panic(pkgError.ErrNotConnected) |
|||
} else if !waCli.IsLoggedIn() { |
|||
panic(pkgError.ErrNotLoggedIn) |
|||
} |
|||
} |
|||
|
|||
func handler(rawEvt interface{}) { |
|||
switch evt := rawEvt.(type) { |
|||
case *events.DeleteForMe: |
|||
log.Infof("Deleted message %s for %s", evt.MessageID, evt.SenderJID.String()) |
|||
case *events.AppStateSyncComplete: |
|||
if len(cli.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock { |
|||
err := cli.SendPresence(types.PresenceAvailable) |
|||
if err != nil { |
|||
log.Warnf("Failed to send available presence: %v", err) |
|||
} else { |
|||
log.Infof("Marked self as available") |
|||
} |
|||
} |
|||
case *events.PairSuccess: |
|||
websocket.Broadcast <- websocket.BroadcastMessage{ |
|||
Code: "LOGIN_SUCCESS", |
|||
Message: fmt.Sprintf("Successfully pair with %s", evt.ID.String()), |
|||
} |
|||
case *events.LoggedOut: |
|||
websocket.Broadcast <- websocket.BroadcastMessage{ |
|||
Code: "LIST_DEVICES", |
|||
Result: nil, |
|||
} |
|||
case *events.Connected, *events.PushNameSetting: |
|||
if len(cli.Store.PushName) == 0 { |
|||
return |
|||
} |
|||
|
|||
// Send presence available when connecting and when the pushname is changed.
|
|||
// This makes sure that outgoing messages always have the right pushname.
|
|||
err := cli.SendPresence(types.PresenceAvailable) |
|||
if err != nil { |
|||
log.Warnf("Failed to send available presence: %v", err) |
|||
} else { |
|||
log.Infof("Marked self as available") |
|||
} |
|||
case *events.StreamReplaced: |
|||
os.Exit(0) |
|||
case *events.Message: |
|||
metaParts := []string{fmt.Sprintf("pushname: %s", evt.Info.PushName), fmt.Sprintf("timestamp: %s", evt.Info.Timestamp)} |
|||
if evt.Info.Type != "" { |
|||
metaParts = append(metaParts, fmt.Sprintf("type: %s", evt.Info.Type)) |
|||
} |
|||
if evt.Info.Category != "" { |
|||
metaParts = append(metaParts, fmt.Sprintf("category: %s", evt.Info.Category)) |
|||
} |
|||
if evt.IsViewOnce { |
|||
metaParts = append(metaParts, "view once") |
|||
} |
|||
|
|||
log.Infof("Received message %s from %s (%s): %+v", evt.Info.ID, evt.Info.SourceString(), strings.Join(metaParts, ", "), evt.Message) |
|||
|
|||
img := evt.Message.GetImageMessage() |
|||
if img != nil { |
|||
path, err := ExtractMedia(config.PathStorages, img) |
|||
if err != nil { |
|||
log.Errorf("Failed to download image: %v", err) |
|||
} else { |
|||
log.Infof("Image downloaded to %s", path) |
|||
} |
|||
} |
|||
|
|||
if config.WhatsappAutoReplyMessage != "" && |
|||
!isGroupJid(evt.Info.Chat.String()) && |
|||
!strings.Contains(evt.Info.SourceString(), "broadcast") { |
|||
_, _ = cli.SendMessage(context.Background(), evt.Info.Sender, &waE2E.Message{Conversation: proto.String(config.WhatsappAutoReplyMessage)}) |
|||
} |
|||
|
|||
if config.WhatsappWebhook != "" && |
|||
!strings.Contains(evt.Info.SourceString(), "broadcast") && |
|||
!isFromMySelf(evt.Info.SourceString()) { |
|||
if err := forwardToWebhook(evt); err != nil { |
|||
logrus.Error("Failed forward to webhook", err) |
|||
} |
|||
} |
|||
case *events.Receipt: |
|||
if evt.Type == types.ReceiptTypeRead || evt.Type == types.ReceiptTypeReadSelf { |
|||
log.Infof("%v was read by %s at %s", evt.MessageIDs, evt.SourceString(), evt.Timestamp) |
|||
} else if evt.Type == types.ReceiptTypeDelivered { |
|||
log.Infof("%s was delivered to %s at %s", evt.MessageIDs[0], evt.SourceString(), evt.Timestamp) |
|||
} |
|||
case *events.Presence: |
|||
if evt.Unavailable { |
|||
if evt.LastSeen.IsZero() { |
|||
log.Infof("%s is now offline", evt.From) |
|||
} else { |
|||
log.Infof("%s is now offline (last seen: %s)", evt.From, evt.LastSeen) |
|||
} |
|||
} else { |
|||
log.Infof("%s is now online", evt.From) |
|||
} |
|||
case *events.HistorySync: |
|||
id := atomic.AddInt32(&historySyncID, 1) |
|||
fileName := fmt.Sprintf("%s/history-%d-%s-%d-%s.json", config.PathStorages, startupTime, cli.Store.ID.String(), id, evt.Data.SyncType.String()) |
|||
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600) |
|||
if err != nil { |
|||
log.Errorf("Failed to open file to write history sync: %v", err) |
|||
return |
|||
} |
|||
enc := json.NewEncoder(file) |
|||
enc.SetIndent("", " ") |
|||
err = enc.Encode(evt.Data) |
|||
if err != nil { |
|||
log.Errorf("Failed to write history sync: %v", err) |
|||
return |
|||
} |
|||
log.Infof("Wrote history sync to %s", fileName) |
|||
_ = file.Close() |
|||
case *events.AppState: |
|||
log.Debugf("App state event: %+v / %+v", evt.Index, evt.SyncActionValue) |
|||
} |
|||
} |
|||
|
|||
func getMessageDigestOrSignature(msg, key []byte) (string, error) { |
|||
mac := hmac.New(sha256.New, key) |
|||
_, err := mac.Write(msg) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
return hex.EncodeToString(mac.Sum(nil)), nil |
|||
} |
|||
|
|||
// forwardToWebhook is a helper function to forward event to webhook url
|
|||
func forwardToWebhook(evt *events.Message) error { |
|||
logrus.Info("Forwarding event to webhook:", config.WhatsappWebhook) |
|||
client := &http.Client{Timeout: 10 * time.Second} |
|||
imageMedia := evt.Message.GetImageMessage() |
|||
stickerMedia := evt.Message.GetStickerMessage() |
|||
videoMedia := evt.Message.GetVideoMessage() |
|||
audioMedia := evt.Message.GetAudioMessage() |
|||
documentMedia := evt.Message.GetDocumentMessage() |
|||
|
|||
var message evtMessage |
|||
message.Text = evt.Message.GetConversation() |
|||
message.ID = evt.Info.ID |
|||
if extendedMessage := evt.Message.ExtendedTextMessage.GetText(); extendedMessage != "" { |
|||
message.Text = extendedMessage |
|||
message.RepliedId = evt.Message.ExtendedTextMessage.ContextInfo.GetStanzaID() |
|||
} |
|||
|
|||
var quotedmessage any |
|||
if evt.Message.ExtendedTextMessage != nil && evt.Message.ExtendedTextMessage.ContextInfo != nil { |
|||
if conversation := evt.Message.ExtendedTextMessage.ContextInfo.QuotedMessage.GetConversation(); conversation != "" { |
|||
quotedmessage = conversation |
|||
} |
|||
} |
|||
|
|||
var forwarded bool |
|||
if evt.Message.ExtendedTextMessage != nil && evt.Message.ExtendedTextMessage.ContextInfo != nil { |
|||
forwarded = evt.Message.ExtendedTextMessage.ContextInfo.GetIsForwarded() |
|||
} |
|||
|
|||
var waReaction evtReaction |
|||
if reactionMessage := evt.Message.ReactionMessage; reactionMessage != nil { |
|||
waReaction.Message = reactionMessage.GetText() |
|||
waReaction.ID = reactionMessage.GetKey().GetID() |
|||
} |
|||
|
|||
body := map[string]interface{}{ |
|||
"audio": audioMedia, |
|||
"contact": evt.Message.GetContactMessage(), |
|||
"document": documentMedia, |
|||
"forwarded": forwarded, |
|||
"from": evt.Info.SourceString(), |
|||
"image": imageMedia, |
|||
"list": evt.Message.GetListMessage(), |
|||
"live_location": evt.Message.GetLiveLocationMessage(), |
|||
"location": evt.Message.GetLocationMessage(), |
|||
"message": message, |
|||
"order": evt.Message.GetOrderMessage(), |
|||
"pushname": evt.Info.PushName, |
|||
"quoted_message": quotedmessage, |
|||
"reaction": waReaction, |
|||
"sticker": stickerMedia, |
|||
"video": videoMedia, |
|||
"view_once": evt.Message.GetViewOnceMessage(), |
|||
} |
|||
|
|||
if imageMedia != nil { |
|||
path, err := ExtractMedia(config.PathMedia, imageMedia) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("Failed to download image: %v", err)) |
|||
} |
|||
body["image"] = path |
|||
} |
|||
if stickerMedia != nil { |
|||
path, err := ExtractMedia(config.PathMedia, stickerMedia) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("Failed to download sticker: %v", err)) |
|||
} |
|||
body["sticker"] = path |
|||
} |
|||
if videoMedia != nil { |
|||
path, err := ExtractMedia(config.PathMedia, videoMedia) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("Failed to download video: %v", err)) |
|||
} |
|||
body["video"] = path |
|||
} |
|||
if audioMedia != nil { |
|||
path, err := ExtractMedia(config.PathMedia, audioMedia) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("Failed to download audio: %v", err)) |
|||
} |
|||
body["audio"] = path |
|||
} |
|||
if documentMedia != nil { |
|||
path, err := ExtractMedia(config.PathMedia, documentMedia) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("Failed to download document: %v", err)) |
|||
} |
|||
body["document"] = path |
|||
} |
|||
|
|||
postBody, err := json.Marshal(body) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("Failed to marshal body: %v", err)) |
|||
} |
|||
|
|||
req, err := http.NewRequest(http.MethodPost, config.WhatsappWebhook, bytes.NewBuffer(postBody)) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("error when create http object %v", err)) |
|||
} |
|||
|
|||
secretKey := []byte(config.WhatsappWebhookSecret) |
|||
signature, err := getMessageDigestOrSignature(postBody, secretKey) |
|||
if err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("error when create signature %v", err)) |
|||
} |
|||
|
|||
req.Header.Set("Content-Type", "application/json") |
|||
req.Header.Set("X-Hub-Signature-256", fmt.Sprintf("sha256=%s", signature)) |
|||
|
|||
if _, err = client.Do(req); err != nil { |
|||
return pkgError.WebhookError(fmt.Sprintf("error when submit webhook %v", err)) |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
// isGroupJid is a helper function to check if the message is from group
|
|||
func isGroupJid(jid string) bool { |
|||
return strings.Contains(jid, "@g.us") |
|||
} |
|||
|
|||
// isFromMySelf is a helper function to check if the message is from my self (logged in account)
|
|||
func isFromMySelf(jid string) bool { |
|||
return extractPhoneNumber(jid) == extractPhoneNumber(cli.Store.ID.String()) |
|||
} |
|||
|
|||
// extractPhoneNumber is a helper function to extract the phone number from a JID
|
|||
func extractPhoneNumber(jid string) string { |
|||
regex := regexp.MustCompile(`\d+`) |
|||
// Find all matches of the pattern in the JID
|
|||
matches := regex.FindAllString(jid, -1) |
|||
// The first match should be the phone number
|
|||
if len(matches) > 0 { |
|||
return matches[0] |
|||
} |
|||
// If no matches are found, return an empty string
|
|||
return "" |
|||
} |
|||
|
|||
// ExtractMedia is a helper function to extract media from whatsapp
|
|||
func ExtractMedia(storageLocation string, mediaFile whatsmeow.DownloadableMessage) (extractedMedia ExtractedMedia, err error) { |
|||
if mediaFile == nil { |
|||
logrus.Info("Skip download because data is nil") |
|||
return extractedMedia, nil |
|||
} |
|||
|
|||
data, err := cli.Download(mediaFile) |
|||
if err != nil { |
|||
return extractedMedia, err |
|||
} |
|||
|
|||
switch media := mediaFile.(type) { |
|||
case *waE2E.ImageMessage: |
|||
extractedMedia.MimeType = media.GetMimetype() |
|||
extractedMedia.Caption = media.GetCaption() |
|||
case *waE2E.AudioMessage: |
|||
extractedMedia.MimeType = media.GetMimetype() |
|||
case *waE2E.VideoMessage: |
|||
extractedMedia.MimeType = media.GetMimetype() |
|||
extractedMedia.Caption = media.GetCaption() |
|||
case *waE2E.StickerMessage: |
|||
extractedMedia.MimeType = media.GetMimetype() |
|||
case *waE2E.DocumentMessage: |
|||
extractedMedia.MimeType = media.GetMimetype() |
|||
extractedMedia.Caption = media.GetCaption() |
|||
} |
|||
|
|||
var extension string |
|||
if ext, err := mime.ExtensionsByType(extractedMedia.MimeType); err != nil && len(ext) > 0 { |
|||
extension = ext[0] |
|||
} else if parts := strings.Split(extractedMedia.MimeType, "/"); len(parts) > 1 { |
|||
extension = "." + parts[len(parts)-1] |
|||
} |
|||
|
|||
extractedMedia.MediaPath = fmt.Sprintf("%s/%d-%s%s", storageLocation, time.Now().Unix(), uuid.NewString(), extension) |
|||
err = os.WriteFile(extractedMedia.MediaPath, data, 0600) |
|||
if err != nil { |
|||
return extractedMedia, err |
|||
} |
|||
return extractedMedia, nil |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue